package edu.princeton;

/******************************************************************************
 * StdDraw3D.java
 * Hayk Martirosyan
 ******************************************************************************
 * 3D Drawing Library
 *
 * Standard Draw 3D is a Java library with the express goal of making it
 * simple to create three-dimensional models, simulations, and games.
 *
 * Introductory Tutorial:
 * http://introcs.cs.princeton.edu/java/stddraw3d
 *
 * Reference manual:
 * http://introcs.cs.princeton.edu/java/stddraw3d-manual.html
 *
 * NOTE: The code below is only partially documented. Refer to the
 * reference manual for complete documentation.
 *
 *****************************************************************************/

// Native Java libraries.
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FileDialog;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Panel;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.text.DecimalFormat;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.Random;
import java.util.Scanner;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BackgroundSound;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.Font3D;
import javax.media.j3d.FontExtrusion;
import javax.media.j3d.Geometry;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.GraphicsContext3D;
import javax.media.j3d.Group;
import javax.media.j3d.ImageComponent;
import javax.media.j3d.ImageComponent2D;
import javax.media.j3d.J3DGraphics2D;
import javax.media.j3d.LineArray;
import javax.media.j3d.LineAttributes;
import javax.media.j3d.LineStripArray;
import javax.media.j3d.LinearFog;
import javax.media.j3d.Material;
import javax.media.j3d.MediaContainer;
import javax.media.j3d.Node;
import javax.media.j3d.PointArray;
import javax.media.j3d.PointAttributes;
import javax.media.j3d.PointLight;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Sound;
import javax.media.j3d.Texture;
import javax.media.j3d.TextureAttributes;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransparencyAttributes;
import javax.media.j3d.TriangleArray;
import javax.media.j3d.TriangleFanArray;
import javax.media.j3d.View;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.vecmath.AxisAngle4d;
import javax.vecmath.Color3f;
import javax.vecmath.Color4f;
import javax.vecmath.Matrix3d;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.SingularMatrixException;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import com.sun.j3d.loaders.Loader;
import com.sun.j3d.loaders.objectfile.ObjectFile;
import com.sun.j3d.utils.behaviors.vp.OrbitBehavior;
import com.sun.j3d.utils.geometry.Cone;
import com.sun.j3d.utils.geometry.Cylinder;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.geometry.Primitive;
import com.sun.j3d.utils.geometry.Sphere;
import com.sun.j3d.utils.image.TextureLoader;
import com.sun.j3d.utils.scenegraph.io.SceneGraphFileReader;
import com.sun.j3d.utils.scenegraph.io.SceneGraphFileWriter;
import com.sun.j3d.utils.scenegraph.io.UnsupportedUniverseException;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.Viewer;
import com.sun.j3d.utils.universe.ViewingPlatform;

/**
 * Standard Draw 3D is a Java library with the express goal of making it simple
 * to create three-dimensional models, simulations, and games. Here is a <a href
 * = "http://introcs.cs.princeton.edu/java/stddraw3d">StdDraw3D tutorial</a> and
 * the <a href =
 * "http://introcs.cs.princeton.edu/java/stddraw3d/stddraw3d-manual.html"
 * >StdDraw3D reference manual</a>.
 */

public final class StdDraw3D implements MouseListener, MouseMotionListener, MouseWheelListener, KeyListener,
		ActionListener, ChangeListener, ComponentListener, WindowFocusListener {

	/* ***************************************************************
	 * Global Variables *
	 * ***************************************************************
	 */

	/* Public constant values. */
	// -------------------------------------------------------------------------

	// Preset colors.
	public static final Color BLACK = Color.BLACK;
	public static final Color BLUE = Color.BLUE;
	public static final Color CYAN = Color.CYAN;
	public static final Color DARK_GRAY = Color.DARK_GRAY;
	public static final Color GRAY = Color.GRAY;
	public static final Color GREEN = Color.GREEN;
	public static final Color LIGHT_GRAY = Color.LIGHT_GRAY;
	public static final Color MAGENTA = Color.MAGENTA;
	public static final Color ORANGE = Color.ORANGE;
	public static final Color PINK = Color.PINK;
	public static final Color RED = Color.RED;
	public static final Color WHITE = Color.WHITE;
	public static final Color YELLOW = Color.YELLOW;

	// Camera modes.
	public static final int ORBIT_MODE = 0;
	public static final int FPS_MODE = 1;
	public static final int AIRPLANE_MODE = 2;
	public static final int LOOK_MODE = 3;
	public static final int FIXED_MODE = 4;
	public static final int IMMERSIVE_MODE = 5;

	/* Global variables. */
	// -------------------------------------------------------------------------

	// GUI Components
	private static JFrame frame;
	private static Panel canvasPanel;
	private static JMenuBar menuBar;
	private static JMenu fileMenu, cameraMenu, graphicsMenu;
	private static JMenuItem loadButton, saveButton, save3DButton, quitButton;
	private static JSpinner fovSpinner;
	private static JRadioButtonMenuItem orbitModeButton, fpsModeButton, airplaneModeButton, lookModeButton,
			fixedModeButton;
	private static JRadioButtonMenuItem perspectiveButton, parallelButton;
	private static JCheckBoxMenuItem antiAliasingButton;
	private static JSpinner numDivSpinner;
	private static JCheckBox infoCheckBox;

	// Scene groups.
	private static SimpleUniverse universe;
	private static BranchGroup rootGroup, lightGroup, soundGroup, fogGroup, appearanceGroup;
	private static BranchGroup onscreenGroup, offscreenGroup;
	private static OrbitBehavior orbit;
	private static Background background;
	private static Group bgGroup;
	private static View view;

	// Drawing canvas.
	private static Canvas3D canvas;

	// Camera object
	private static Camera camera;

	// Buffered Images for 2D drawing
	private static BufferedImage offscreenImage, onscreenImage;
	private static BufferedImage infoImage;

	// Canvas dimensions.
	private static int width;
	private static int height;
	private static double aspectRatio;

	// Camera mode.
	private static int cameraMode;

	// Center of orbit
	private static Point3d orbitCenter;

	// Coordinate bounds.
	private static double min, max, zoom;

	// Current background color.
	private static Color bgColor;

	// Pen properties.
	private static Color penColor;
	private static float penRadius;
	private static Font font;

	// Keeps track of screen clearing.
	private static boolean clear3D;
	private static boolean clearOverlay;
	private static boolean infoDisplay;

	// Number of triangles per shape.
	private static int numDivisions;

	// Mouse states.
	private static boolean mouse1;
	private static boolean mouse2;
	private static boolean mouse3;
	private static double mouseX;
	private static double mouseY;

	// Keyboard states.
	private static TreeSet<Integer> keysDown = new TreeSet<Integer>();
	private static LinkedList<Character> keysTyped = new LinkedList<Character>();

	// For event synchronization.
	private static Object mouseLock = new Object();
	private static Object keyLock = new Object();

	// Helper to see when initialization complete
	private static boolean initialized = false;
	private static boolean fullscreen = false;
	private static boolean immersive = false;

	// Pauses the renderer
	private static boolean showedOnce = true;
	private static boolean renderedOnce = false;

	/* Final variables for default values. */
	// -------------------------------------------------------------------------

	// Default square canvas dimension in pixels.
	private static final int DEFAULT_SIZE = 600;

	// Default boundaries of canvas scale.
	private static final double DEFAULT_MIN = 0.0;
	private static final double DEFAULT_MAX = 1.0;

	// Default camera mode
	private static final int DEFAULT_CAMERA_MODE = ORBIT_MODE;

	// Default field of vision for perspective projection.
	private static final double DEFAULT_FOV = 0.9;
	private static final int DEFAULT_NUM_DIVISIONS = 100;

	// Default clipping distances for rendering.
	private static final double DEFAULT_FRONT_CLIP = 0.01;
	private static final double DEFAULT_BACK_CLIP = 10;

	// Default pen settings.
	private static final Font DEFAULT_FONT = new Font("Arial", Font.PLAIN, 16);
	private static final double DEFAULT_PEN_RADIUS = 0.002;
	private static final Color DEFAULT_PEN_COLOR = StdDraw3D.WHITE;

	// Default background color.
	private static final Color DEFAULT_BGCOLOR = StdDraw3D.BLACK;

	// Scales the size of Text3D.
	private static final double TEXT3D_SHRINK_FACTOR = 0.005;
	private static final double TEXT3D_DEPTH = 1.5;

	// Default shape flags.
	private static final int PRIMFLAGS = Primitive.GENERATE_NORMALS + Primitive.GENERATE_TEXTURE_COORDS;

	// Infinite bounding sphere.
	private static final BoundingSphere INFINITE_BOUNDS = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 1e100);

	// Axis vectors.
	private static final Vector3D xAxis = new Vector3D(1, 0, 0);
	private static final Vector3D yAxis = new Vector3D(0, 1, 0);
	private static final Vector3D zAxis = new Vector3D(0, 0, 1);

	/* Housekeeping. */
	// -------------------------------------------------------------------------

	// Singleton for callbacks - avoids generation of extra .class files.
	private static StdDraw3D std = new StdDraw3D();

	// Blank constructor.
	private StdDraw3D() {
	}

	// Static initializer.
	static {
		// System.setProperty("j3d.rend", "ogl");
		System.setProperty("j3d.audiodevice", "com.sun.j3d.audioengines.javasound.JavaSoundMixer");
		setCanvasSize(DEFAULT_SIZE, DEFAULT_SIZE);
	}

	/* ***************************************************************
	 * Initialization Methods *
	 * ***************************************************************
	 */

	/**
	 * Initializes the 3D engine.
	 */
	private static void initialize() {

		numDivisions = DEFAULT_NUM_DIVISIONS;

		onscreenImage = createBufferedImage();
		offscreenImage = createBufferedImage();
		infoImage = createBufferedImage();

		initializeCanvas();

		if (frame != null)
			frame.setVisible(false);
		frame = new JFrame();
		frame.setVisible(false);
		frame.setResizable(fullscreen);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setTitle("Standard Draw 3D");
		frame.add(canvasPanel);
		frame.setJMenuBar(createMenuBar());
		frame.addComponentListener(std);
		frame.addWindowFocusListener(std);
		// frame.getContentPane().setCursor(new Cursor(Cursor.MOVE_CURSOR));
		frame.pack();

		rootGroup = createBranchGroup();
		lightGroup = createBranchGroup();
		bgGroup = createBranchGroup();
		soundGroup = createBranchGroup();
		fogGroup = createBranchGroup();
		appearanceGroup = createBranchGroup();

		onscreenGroup = createBranchGroup();
		offscreenGroup = createBranchGroup();

		rootGroup.addChild(onscreenGroup);
		rootGroup.addChild(lightGroup);
		rootGroup.addChild(bgGroup);
		rootGroup.addChild(soundGroup);
		rootGroup.addChild(fogGroup);
		rootGroup.addChild(appearanceGroup);

		universe = new SimpleUniverse(canvas, 2);
		universe.addBranchGraph(rootGroup);

		setDefaultLight();

		Viewer viewer = universe.getViewer();
		viewer.createAudioDevice();

		view = viewer.getView();
		view.setTransparencySortingPolicy(View.TRANSPARENCY_SORT_GEOMETRY);
		view.setScreenScalePolicy(View.SCALE_EXPLICIT);
		view.setLocalEyeLightingEnable(true);
		setAntiAliasing(false);

		// view.setMinimumFrameCycleTime(long minimumTime);

		ViewingPlatform viewingPlatform = universe.getViewingPlatform();
		viewingPlatform.setNominalViewingTransform();

		orbit = new OrbitBehavior(canvas, OrbitBehavior.REVERSE_ALL ^ OrbitBehavior.STOP_ZOOM);
		BoundingSphere bounds = INFINITE_BOUNDS;
		orbit.setMinRadius(0);
		orbit.setSchedulingBounds(bounds);
		setOrbitCenter(new Point3d(0, 0, 0));

		viewingPlatform.setViewPlatformBehavior(orbit);
		TransformGroup cameraTG = viewingPlatform.getViewPlatformTransform();

		Transform3D cameraTrans = new Transform3D();
		cameraTG.getTransform(cameraTrans);

		viewingPlatform.detach();
		cameraTG.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		cameraTG.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		camera = new Camera(cameraTG);
		universe.addBranchGraph(viewingPlatform);

		setPerspectiveProjection();
		setCameraMode();
		setPenColor();
		setPenRadius();
		setFont();
		setScale();
		setInfoDisplay(true);

		setBackground(DEFAULT_BGCOLOR);

		frame.setVisible(true);
		frame.toFront();
		frame.setState(Frame.NORMAL);
		initialized = true;

	}

	public static boolean isInitialized() {
		return initialized;
	}

	public static boolean isShowedOnce() {
		return showedOnce;
	}

	public static boolean isRenderedOnce() {
		return renderedOnce;
	}

	/**
	 * Adds a Canvas3D to the given Panel p.
	 */
	private static void initializeCanvas() {

		Panel p = new Panel();

		GridBagLayout gl = new GridBagLayout();
		GridBagConstraints gbc = new GridBagConstraints();

		p.setLayout(gl);
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.gridwidth = 5;
		gbc.gridheight = 5;

		GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();

		canvas = new Canvas3D(config) {

			private static final long serialVersionUID = -4375786077908331756L;

			public void preRender() {
				if (camera.pair != null) {
					camera.setPosition(camera.pair.getPosition());
					camera.setOrientation(camera.pair.getOrientation());
				}
			}

			public void postRender() {
				J3DGraphics2D graphics = this.getGraphics2D();

				graphics.drawRenderedImage(onscreenImage, new AffineTransform());
				if (infoDisplay) {
					graphics.drawRenderedImage(infoImage, new AffineTransform());
				}

				graphics.flush(false);

				/*
				 * while (!showedOnce) {
				 * 
				 * try { Thread.currentThread().sleep(0); } catch
				 * (InterruptedException e) {
				 * System.out.println("Error sleeping"); } } showedOnce = false;
				 */

				Thread.yield();
			}

			/*
			 * public void postSwap () { while (!showedOnce) {
			 * 
			 * try { Thread.currentThread().sleep(5); } catch
			 * (InterruptedException e) { System.out.println("Error sleeping");
			 * } } //if (!showedOnce) { // try {
			 * Thread.currentThread().sleep(30); } // catch
			 * (InterruptedException e) { System.out.println("Error sleeping");
			 * } //} //StdOut.println("showed once is false, rendered once");
			 * showedOnce = false; //renderedOnce = true;
			 * 
			 * //Thread.yield(); }
			 */
		};

		canvas.addKeyListener(std);
		canvas.addMouseListener(std);
		canvas.addMouseMotionListener(std);
		canvas.addMouseWheelListener(std);

		canvas.setSize(width, height);
		p.add(canvas, gbc);

		canvasPanel = p;

		// canvas.stopRenderer();
	}

	/**
	 * Creates a menu bar as a basic GUI.
	 * 
	 * @return The created JMenuBar.
	 */
	private static JMenuBar createMenuBar() {

		menuBar = new JMenuBar();

		fileMenu = new JMenu("File");
		menuBar.add(fileMenu);

		loadButton = new JMenuItem(" Load 3D Model..  ");
		loadButton.addActionListener(std);
		loadButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));
		fileMenu.add(loadButton);

		saveButton = new JMenuItem(" Save Image...  ");
		saveButton.addActionListener(std);
		saveButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));
		fileMenu.add(saveButton);

		save3DButton = new JMenuItem(" Export 3D Scene...  ");
		save3DButton.addActionListener(std);
		save3DButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));
		// fileMenu.add(save3DButton);

		fileMenu.addSeparator();

		quitButton = new JMenuItem(" Quit...   ");
		quitButton.addActionListener(std);
		quitButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));
		fileMenu.add(quitButton);

		cameraMenu = new JMenu("Camera");
		menuBar.add(cameraMenu);

		JLabel cameraLabel = new JLabel("Camera Mode");
		cameraLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
		cameraLabel.setForeground(GRAY);
		cameraMenu.add(cameraLabel);
		cameraMenu.addSeparator();

		ButtonGroup cameraButtonGroup = new ButtonGroup();

		orbitModeButton = new JRadioButtonMenuItem("Orbit Mode");
		orbitModeButton.setSelected(true);
		cameraButtonGroup.add(orbitModeButton);
		cameraMenu.add(orbitModeButton);
		orbitModeButton.addActionListener(std);
		orbitModeButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_1, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));

		fpsModeButton = new JRadioButtonMenuItem("First-Person Mode");
		cameraButtonGroup.add(fpsModeButton);
		cameraMenu.add(fpsModeButton);
		fpsModeButton.addActionListener(std);
		fpsModeButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_2, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));

		airplaneModeButton = new JRadioButtonMenuItem("Airplane Mode");
		cameraButtonGroup.add(airplaneModeButton);
		cameraMenu.add(airplaneModeButton);
		airplaneModeButton.addActionListener(std);
		airplaneModeButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_3, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));

		lookModeButton = new JRadioButtonMenuItem("Look Mode");
		cameraButtonGroup.add(lookModeButton);
		cameraMenu.add(lookModeButton);
		lookModeButton.addActionListener(std);
		lookModeButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_4, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));

		fixedModeButton = new JRadioButtonMenuItem("Fixed Mode");
		cameraButtonGroup.add(fixedModeButton);
		cameraMenu.add(fixedModeButton);
		fixedModeButton.addActionListener(std);
		fixedModeButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_5, Toolkit.getDefaultToolkit()
				.getMenuShortcutKeyMask()));

		cameraMenu.addSeparator();
		JLabel projectionLabel = new JLabel("Projection Mode");
		projectionLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
		projectionLabel.setForeground(GRAY);
		cameraMenu.add(projectionLabel);
		cameraMenu.addSeparator();

		SpinnerNumberModel snm = new SpinnerNumberModel(DEFAULT_FOV, 0.5, 3.0, 0.05);
		fovSpinner = new JSpinner(snm);
		JPanel fovPanel = new JPanel();
		fovPanel.setLayout(new BoxLayout(fovPanel, BoxLayout.X_AXIS));
		JLabel fovLabel = new JLabel("Field of View:");
		fovPanel.add(javax.swing.Box.createRigidArea(new Dimension(30, 5)));
		fovPanel.add(fovLabel);
		fovPanel.add(javax.swing.Box.createRigidArea(new Dimension(10, 5)));
		fovPanel.add(fovSpinner);

		final ButtonGroup projectionButtons = new ButtonGroup();
		perspectiveButton = new JRadioButtonMenuItem("Perspective Projection");
		parallelButton = new JRadioButtonMenuItem("Parallel Projection");

		fovSpinner.addChangeListener(std);

		perspectiveButton.addActionListener(std);

		parallelButton.addActionListener(std);

		cameraMenu.add(parallelButton);
		cameraMenu.add(perspectiveButton);
		cameraMenu.add(fovPanel);

		projectionButtons.add(parallelButton);
		projectionButtons.add(perspectiveButton);
		perspectiveButton.setSelected(true);

		graphicsMenu = new JMenu("Graphics");
		// Leaving out graphics menu for now!!
		// menuBar.add(graphicsMenu);

		JLabel graphicsLabel = new JLabel("Polygon Count");
		graphicsLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
		graphicsLabel.setForeground(GRAY);
		graphicsMenu.add(graphicsLabel);
		graphicsMenu.addSeparator();

		SpinnerNumberModel snm2 = new SpinnerNumberModel(DEFAULT_NUM_DIVISIONS, 4, 4000, 5);
		numDivSpinner = new JSpinner(snm2);

		JPanel numDivPanel = new JPanel();
		numDivPanel.setLayout(new BoxLayout(numDivPanel, BoxLayout.X_AXIS));
		JLabel numDivLabel = new JLabel("Triangles:");
		numDivPanel.add(javax.swing.Box.createRigidArea(new Dimension(5, 5)));
		numDivPanel.add(numDivLabel);
		numDivPanel.add(javax.swing.Box.createRigidArea(new Dimension(15, 5)));
		numDivPanel.add(numDivSpinner);
		graphicsMenu.add(numDivPanel);

		numDivSpinner.addChangeListener(std);

		graphicsMenu.addSeparator();
		JLabel graphicsLabel2 = new JLabel("Advanced Rendering");
		graphicsLabel2.setAlignmentX(Component.CENTER_ALIGNMENT);
		graphicsLabel2.setForeground(GRAY);
		graphicsMenu.add(graphicsLabel2);
		graphicsMenu.addSeparator();

		antiAliasingButton = new JCheckBoxMenuItem("Enable Anti-Aliasing");
		antiAliasingButton.setSelected(false);
		antiAliasingButton.addActionListener(std);

		graphicsMenu.add(antiAliasingButton);

		infoCheckBox = new JCheckBox("Show Info Display");
		infoCheckBox.setFocusable(false);
		infoCheckBox.addActionListener(std);
		menuBar.add(javax.swing.Box.createRigidArea(new Dimension(50, 5)));
		menuBar.add(infoCheckBox);

		return menuBar;
	}

	public void stateChanged(ChangeEvent e) {
		Object source = e.getSource();

		if (source == numDivSpinner)
			numDivisions = (Integer) numDivSpinner.getValue();
		if (source == fovSpinner) {
			setPerspectiveProjection((Double) fovSpinner.getValue());
			perspectiveButton.setSelected(true);
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();

		if (source == saveButton)
			saveAction();
		else if (source == loadButton)
			loadAction();
		else if (source == save3DButton)
			save3DAction();
		else if (source == quitButton)
			quitAction();
		else if (source == orbitModeButton)
			setCameraMode(ORBIT_MODE);
		else if (source == fpsModeButton)
			setCameraMode(FPS_MODE);
		else if (source == airplaneModeButton)
			setCameraMode(AIRPLANE_MODE);
		else if (source == lookModeButton)
			setCameraMode(LOOK_MODE);
		else if (source == fixedModeButton)
			setCameraMode(FIXED_MODE);
		else if (source == perspectiveButton)
			setPerspectiveProjection((Double) fovSpinner.getValue());
		else if (source == parallelButton)
			setParallelProjection();
		else if (source == antiAliasingButton)
			setAntiAliasing(antiAliasingButton.isSelected());
		else if (source == infoCheckBox)
			setInfoDisplay(infoCheckBox.isSelected());
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void componentHidden(ComponentEvent e) {
		keysDown = new TreeSet<Integer>();
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void componentMoved(ComponentEvent e) {
		keysDown = new TreeSet<Integer>();
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void componentShown(ComponentEvent e) {
		keysDown = new TreeSet<Integer>();
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void componentResized(ComponentEvent e) {
		keysDown = new TreeSet<Integer>();
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void windowGainedFocus(WindowEvent e) {
		keysDown = new TreeSet<Integer>();
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void windowLostFocus(WindowEvent e) {
		keysDown = new TreeSet<Integer>();
	}

	/**
	 * Creates a blank BranchGroup with the proper capabilities.
	 */
	private static BranchGroup createBranchGroup() {

		BranchGroup bg = new BranchGroup();
		bg.setCapability(BranchGroup.ALLOW_CHILDREN_READ);
		bg.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
		bg.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
		bg.setCapability(BranchGroup.ALLOW_DETACH);
		bg.setPickable(false);
		bg.setCollidable(false);
		return bg;
	}

	/**
	 * Creates a blank TransformGroup with the proper capabilities.
	 */
	private static TransformGroup createTransformGroup() {

		TransformGroup tg = new TransformGroup();
		tg.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		tg.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		tg.setPickable(false);
		tg.setCollidable(false);
		return tg;
	}

	/**
	 * Creates a blank Background with the proper capabilities.
	 * 
	 * @return The created Background.
	 */
	private static Background createBackground() {

		Background background = new Background();
		background.setCapability(Background.ALLOW_COLOR_WRITE);
		background.setCapability(Background.ALLOW_IMAGE_WRITE);
		background.setCapability(Background.ALLOW_GEOMETRY_WRITE);
		background.setApplicationBounds(INFINITE_BOUNDS);
		return background;
	}

	/**
	 * Creates a texture from the given filename.
	 * 
	 * @param imageURL
	 *            Image file for creating texture.
	 * @return The created Texture.
	 * @throws RuntimeException
	 *             if the file could not be read.
	 */
	private static Texture createTexture(String imageURL) {

		TextureLoader loader;
		try {
			loader = new TextureLoader(imageURL, "RGBA", TextureLoader.Y_UP, new Container());
		} catch (Exception e) {
			throw new RuntimeException("Could not read from the file '" + imageURL + "'");
		}

		Texture texture = loader.getTexture();
		texture.setBoundaryModeS(Texture.WRAP);
		texture.setBoundaryModeT(Texture.WRAP);
		texture.setBoundaryColor(new Color4f(0.0f, 1.0f, 0.0f, 0.0f));

		return texture;
	}

	private static Appearance createBlankAppearance() {
		Appearance ap = new Appearance();
		ap.setCapability(Appearance.ALLOW_MATERIAL_READ);
		ap.setCapability(Appearance.ALLOW_MATERIAL_WRITE);
		ap.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_READ);
		ap.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_WRITE);
		return ap;
	}

	/**
	 * Creates an Appearance.
	 * 
	 * @param imageURL
	 *            Wraps a texture around from this image file.
	 * @param fill
	 *            If true, fills in faces. If false, outlines.
	 * @return The created appearance.
	 */
	private static Appearance createAppearance(String imageURL, boolean fill) {

		Appearance ap = createBlankAppearance();

		PolygonAttributes pa = new PolygonAttributes();
		if (!fill)
			pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
		pa.setCullFace(PolygonAttributes.CULL_NONE);
		ap.setPolygonAttributes(pa);

		if (imageURL != null) {

			Texture texture = createTexture(imageURL);
			TextureAttributes texAttr = new TextureAttributes();
			texAttr.setTextureMode(TextureAttributes.REPLACE);

			ap.setTexture(texture);
			ap.setTextureAttributes(texAttr);
		}

		Color3f col = new Color3f(penColor);
		Color3f black = new Color3f(0, 0, 0);
		Color3f specular = new Color3f(GRAY);

		// Material properties
		Material material = new Material(col, black, col, specular, 64);
		material.setCapability(Material.ALLOW_COMPONENT_READ);
		material.setCapability(Material.ALLOW_COMPONENT_WRITE);
		material.setLightingEnable(true);
		ap.setMaterial(material);

		// Transparecy properties
		float alpha = ((float) penColor.getAlpha()) / 255;
		if (alpha < 1.0) {
			TransparencyAttributes t = new TransparencyAttributes();
			t.setTransparencyMode(TransparencyAttributes.BLENDED);
			t.setTransparency(1 - alpha);
			ap.setTransparencyAttributes(t);
		}

		LineAttributes la = new LineAttributes();
		la.setLineWidth(penRadius);
		la.setLineAntialiasingEnable(view.getSceneAntialiasingEnable());

		PointAttributes poa = new PointAttributes();
		poa.setPointAntialiasingEnable(view.getSceneAntialiasingEnable());

		ColoringAttributes ca = new ColoringAttributes();
		ca.setShadeModel(ColoringAttributes.SHADE_GOURAUD);
		ca.setColor(col);

		ap.setLineAttributes(la);
		ap.setPointAttributes(poa);
		ap.setColoringAttributes(ca);

		return ap;
	}

	// FIX THIS check that adding specular didn't mess up custom shapes (lines,
	// triangles, points)
	private static Appearance createCustomAppearance(boolean fill) {
		Appearance ap = createBlankAppearance();

		PolygonAttributes pa = new PolygonAttributes();
		if (!fill)
			pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
		pa.setCullFace(PolygonAttributes.CULL_NONE);

		LineAttributes la = new LineAttributes();
		la.setLineWidth(penRadius);
		la.setLineAntialiasingEnable(view.getSceneAntialiasingEnable());

		PointAttributes poa = new PointAttributes();
		poa.setPointAntialiasingEnable(view.getSceneAntialiasingEnable());

		ap.setPolygonAttributes(pa);
		ap.setLineAttributes(la);
		ap.setPointAttributes(poa);

		Color3f col = new Color3f(penColor);
		Color3f black = new Color3f(0, 0, 0);
		Color3f specular = new Color3f(GRAY);

		// Material properties
		Material material = new Material(col, black, col, specular, 64);
		material.setCapability(Material.ALLOW_COMPONENT_READ);
		material.setCapability(Material.ALLOW_COMPONENT_WRITE);
		material.setLightingEnable(true);
		ap.setMaterial(material);

		return ap;
	}

	private static Shape3D createShape3D(Geometry geom) {
		Shape3D shape = new Shape3D(geom);
		shape.setPickable(false);
		shape.setCollidable(false);
		shape.setCapability(Shape3D.ALLOW_APPEARANCE_READ);
		shape.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE);
		shape.setCapability(Shape3D.ALLOW_APPEARANCE_OVERRIDE_READ);
		shape.setCapability(Shape3D.ALLOW_APPEARANCE_OVERRIDE_WRITE);

		return shape;
	}

	/**
	 * Creates a blank BufferedImage.
	 * 
	 * @return The created BufferedImage.
	 */
	private static BufferedImage createBufferedImage() {
		return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
	}

	/** Converts a Vector3D to a Vector3d. */
	private static Vector3d createVector3d(Vector3D v) {
		return new Vector3d(v.x, v.y, v.z);
	}

	/** Converts three double scalars to a Vector3f. **/
	private static Vector3f createVector3f(double x, double y, double z) {
		return new Vector3f((float) x, (float) y, (float) z);
	}

	/** Converts a Vector3D to a Vector3f. */
	private static Vector3f createVector3f(Vector3D v) {
		return createVector3f(v.x, v.y, v.z);
	}

	/** Converts a Vector3D to a Point3f. */
	// private static Point3f createPoint3f (Vector3D v) {
	// return createPoint3f(v.x, v.y, v.z);
	// }

	/** Converts three double scalars to a Point3f. */
	private static Point3f createPoint3f(double x, double y, double z) {
		return new Point3f((float) x, (float) y, (float) z);
	}

	/**
	 * Creates a scanner from the filename or website name.
	 */
	private static Scanner createScanner(String s) {

		Scanner scanner;
		String charsetName = "ISO-8859-1";
		java.util.Locale usLocale = new java.util.Locale("en", "US");
		try {
			// first try to read file from local file system
			File file = new File(s);
			if (file.exists()) {
				scanner = new Scanner(file, charsetName);
				scanner.useLocale(usLocale);
				return scanner;
			}

			// next try for website
			URL url = new URL(s);

			URLConnection site = url.openConnection();
			InputStream is = site.getInputStream();
			scanner = new Scanner(new BufferedInputStream(is), charsetName);
			scanner.useLocale(usLocale);
			return scanner;
		} catch (IOException ioe) {
			System.err.println("Could not open " + s + ".");
			return null;
		}
	}

	/* ***************************************************************
	 * Scaling and Screen Methods *
	 * ***************************************************************
	 */

	/**
	 * Sets the window size to w-by-h pixels.
	 * 
	 * @param w
	 *            The width as a number of pixels.
	 * @param h
	 *            The height as a number of pixels.
	 * @throws a
	 *             RuntimeException if the width or height is 0 or negative.
	 */
	public static void setCanvasSize(int w, int h) {

		setCanvasSize(w, h, false);
	}

	private static void setCanvasSize(int w, int h, boolean fs) {

		fullscreen = fs;

		if (w < 1 || h < 1)
			throw new RuntimeException("Dimensions must be positive integers!");
		width = w;
		height = h;

		aspectRatio = (double) width / (double) height;
		initialize();
	}

	/**
	 * Sets the default scale for all three dimensions.
	 */
	public static void setScale() {
		setScale(DEFAULT_MIN, DEFAULT_MAX);
	}

	/**
	 * Sets the scale for all three dimensions.
	 * 
	 * @param minimum
	 *            The minimum value of each scale.
	 * @param maximum
	 *            The maximum value of each scale.
	 */
	public static void setScale(double minimum, double maximum) {

		min = minimum;
		max = maximum;
		zoom = (max - min) / 2;
		double center = min + zoom;

		// double nominalDist = camera.getPosition().z;
		camera.setPosition(center, center, zoom * (2 + Math.sqrt(2)));

		double orbitScale = 0.5 * zoom;

		orbit.setZoomFactor(orbitScale);
		orbit.setTransFactors(orbitScale, orbitScale);

		setOrbitCenter(new Point3d(center, center, center));

		view.setFrontClipDistance(DEFAULT_FRONT_CLIP * zoom);
		view.setBackClipDistance(DEFAULT_BACK_CLIP * zoom);
	}

	/**
	 * Scales the given x-coordinate from user coordinates into 2D pixel
	 * coordinates.
	 */
	private static float scaleX(double x) {

		double scale = 1;
		if (width > height)
			scale = 1 / aspectRatio;

		return (float) (width * (x * scale - min) / (2 * zoom));
	}

	/**
	 * Scales the given y-coordinate from user coordinates into 2D pixel
	 * coordinates.
	 */
	private static float scaleY(double y) {

		double scale = 1;
		if (height > width)
			scale = aspectRatio;

		return (float) (height * (max - y * scale) / (2 * zoom));
	}

	/**
	 * Scales the given width from user coordinates into 2D pixel coordinates.
	 */
	private static double factorX(double w) {

		double scaleDist = width;
		if (width > height)
			scaleDist = height;

		return scaleDist * (w / (2 * zoom));
	}

	/**
	 * Scales the given height from user coordinates into 2D pixel coordinates.
	 */
	private static double factorY(double h) {

		double scaleDist = height;
		if (height > width)
			scaleDist = width;

		return scaleDist * (h / (2 * zoom));
	}

	/**
	 * Scales the given x-coordinate from 2D pixel coordinates into user
	 * coordinates.
	 */
	private static double unscaleX(double xs) {

		double scale = 1;
		if (width > height)
			scale = 1 / aspectRatio;

		return (xs * (2 * zoom) / width + min) / scale;
	}

	/**
	 * Scales the given y-coordinate from 2D pixel coordinates into user
	 * coordinates.
	 */
	private static double unscaleY(double ys) {

		double scale = 1;
		if (height > width)
			scale = aspectRatio;
		// System.out.println("unscaleY scale = " + scale);
		return (max - ys * (2 * zoom) / height) / scale;
	}

	/* ***************************************************************
	 * Pen Properties Methods *
	 * ***************************************************************
	 */

	/**
	 * Sets the pen color to the given color and transparency.
	 * 
	 * @param col
	 *            The given opaque color.
	 * @param alpha
	 *            The transparency value (0-255).
	 */
	public static void setPenColor(Color col, int alpha) {
		setPenColor(new Color(col.getRed(), col.getGreen(), col.getBlue(), alpha));
	}

	/**
	 * Sets the pen color to the default.
	 */
	public static void setPenColor() {
		penColor = DEFAULT_PEN_COLOR;
	}

	/**
	 * Sets the pen color to the given color.
	 * 
	 * @param col
	 *            The given color.
	 */
	public static void setPenColor(Color col) {
		penColor = col;
	}

	public static void setPenColor(int r, int g, int b) {
		penColor = new Color(r, g, b);
	}

	/**
	 * Returns the current pen color.
	 * 
	 * @return The current pen color.
	 */
	public static Color getPenColor() {
		return penColor;
	}

	/**
	 * Sets the pen radius to the default value.
	 */
	public static void setPenRadius() {
		setPenRadius(DEFAULT_PEN_RADIUS);
	}

	/**
	 * Sets the pen radius to the given value.
	 * 
	 * @param r
	 *            The pen radius.
	 */
	public static void setPenRadius(double r) {
		penRadius = (float) r * 500;
	}

	/**
	 * Gets the current pen radius.
	 * 
	 * @return The current pen radius.
	 */
	public static float getPenRadius() {
		return penRadius / 500f;
	}

	/**
	 * Sets the default font.
	 */
	public static void setFont() {
		font = DEFAULT_FONT;
	}

	/**
	 * Sets the font to draw text with.
	 * 
	 * @param f
	 *            The font to set.
	 */
	public static void setFont(Font f) {
		font = f;
	}

	/**
	 * Gets the current drawing Font.
	 * 
	 * @return The current Font.
	 */
	public static Font getFont() {
		return font;
	}

	public static void setInfoDisplay(boolean enabled) {
		infoDisplay = enabled;
		infoCheckBox.setSelected(enabled);
		camera.move(0, 0, 0);
		infoDisplay();
	}

	/* ***************************************************************
	 * View Properties Methods *
	 * ***************************************************************
	 */

	public static void fullscreen() {

		frame.setResizable(true);
		frame.setExtendedState(Frame.MAXIMIZED_BOTH);
		int w = frame.getSize().width;
		int h = frame.getSize().height;

		// int w = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
		// int h = (int)
		// Toolkit.getDefaultToolkit().getScreenSize().getHeight();

		int borderY = frame.getInsets().top + frame.getInsets().bottom;
		int borderX = frame.getInsets().left + frame.getInsets().right;

		setCanvasSize(w - borderX, h - borderY - menuBar.getHeight(), true);
		frame.setExtendedState(Frame.MAXIMIZED_BOTH);
	}

	/**
	 * Sets anti-aliasing on or off. Anti-aliasing makes graphics look much
	 * smoother, but is very resource heavy. It is good for saving images that
	 * look professional. The default is off.
	 * 
	 * @param enabled
	 *            The state of anti-aliasing.
	 */
	public static void setAntiAliasing(boolean enabled) {
		view.setSceneAntialiasingEnable(enabled);
		antiAliasingButton.setSelected(enabled);
		// System.out.println("Anti aliasing enabled: " + enabled);
	}

	/**
	 * Returns true if anti-aliasing is enabled.
	 */
	public static boolean getAntiAliasing() {
		return antiAliasingButton.isSelected();
	}

	/**
	 * Sets the number of triangular divisons of curved objects. The default is
	 * 100 divisons. Decrease this to increase performance.
	 * 
	 * @param N
	 *            The number of divisions.
	 */
	public static void setNumDivisions(int N) {
		numDivisions = N;
	}

	/**
	 * Gets the number of triangular divisons of curved objects.
	 * 
	 * @return The number of divisions.
	 */
	public static int getNumDivisions() {
		return numDivisions;
	}

	/**
	 * Gets the current camera mode.
	 * 
	 * @return The current camera mode.
	 */
	public static int getCameraMode() {
		return cameraMode;
	}

	/**
	 * Sets the current camera mode. <br>
	 * -------------------------------------- <br>
	 * StdDraw3D.ORBIT_MODE: <br>
	 * Rotates and zooms around a central point. <br>
	 * <br>
	 * Mouse Click Left - Orbit <br>
	 * Mouse Click Right - Pan <br>
	 * Mouse Wheel / Alt-Click - Zoom <br>
	 * -------------------------------------- <br>
	 * StdDraw3D.FPS_MODE: <br>
	 * First-person-shooter style controls. <br>
	 * Up is always the positive y-axis. <br>
	 * <br>
	 * W/Up - Forward <br>
	 * S/Down - Backward <br>
	 * A/Left - Left <br>
	 * D/Right - Right <br>
	 * Q/Page Up - Up <br>
	 * E/Page Down - Down <br>
	 * Mouse - Look <br>
	 * -------------------------------------- <br>
	 * StdDraw3D.AIRPLANE_MODE: <br>
	 * Similar to fps_mode, but can be oriented <br>
	 * with up in any direction. <br>
	 * <br>
	 * W/Up - Forward <br>
	 * S/Down - Backward <br>
	 * A/Left - Left <br>
	 * D/Right - Right <br>
	 * Q/Page Up - Rotate CCW <br>
	 * E/Page Down - Rotate CW <br>
	 * Mouse - Look <br>
	 * -------------------------------------- <br>
	 * StdDraw3D.LOOK_MODE: <br>
	 * No movement, but uses mouse to look around. <br>
	 * <br>
	 * Mouse - Look <br>
	 * -------------------------------------- <br>
	 * StdDraw3D.FIXED_MODE: <br>
	 * No movement or looking. <br>
	 * -------------------------------------- <br>
	 * 
	 * @param mode
	 *            The camera mode.
	 */
	public static void setCameraMode(int mode) {

		cameraMode = mode;

		if (cameraMode == ORBIT_MODE) {
			orbit.setRotateEnable(true);
			if (view.getProjectionPolicy() != View.PARALLEL_PROJECTION)
				orbit.setZoomEnable(true);
			orbit.setTranslateEnable(true);
			orbit.setRotationCenter(orbitCenter);
			orbitModeButton.setSelected(true);
		} else {
			orbit.setRotateEnable(false);
			orbit.setZoomEnable(false);
			orbit.setTranslateEnable(false);
		}
		if (cameraMode == FPS_MODE) {
			fpsModeButton.setSelected(true);
			camera.rotateFPS(0, 0, 0);
		}
		if (cameraMode == AIRPLANE_MODE) {
			airplaneModeButton.setSelected(true);
		}
		if (cameraMode == LOOK_MODE) {
			// System.out.println("Camera in look mode.");
			lookModeButton.setSelected(true);
		}
		if (cameraMode == FIXED_MODE) {
			// System.out.println("Camera in fixed mode.");
			fixedModeButton.setSelected(true);
		}
		if (cameraMode == IMMERSIVE_MODE) {

			BufferedImage cursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
			Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new java.awt.Point(0, 0),
					"blank cursor");
			frame.getContentPane().setCursor(blankCursor);
			// System.out.println("Camera in fps mode.");
		} else {
			frame.getContentPane().setCursor(Cursor.getDefaultCursor());
		}
	}

	/**
	 * Sets the default camera mode.
	 */
	public static void setCameraMode() {
		setCameraMode(DEFAULT_CAMERA_MODE);
	}

	/**
	 * Sets the center of rotation for the camera mode ORBIT_MODE.
	 */
	public static void setOrbitCenter(double x, double y, double z) {
		setOrbitCenter(new Point3d(x, y, z));
	}

	/**
	 * Sets the center of rotation for the camera mode ORBIT_MODE.
	 */
	public static void setOrbitCenter(Vector3D v) {
		setOrbitCenter(new Point3d(v.x, v.y, v.z));
	}

	private static void setOrbitCenter(Point3d center) {
		orbitCenter = center;
		orbit.setRotationCenter(orbitCenter);
	}

	public static Vector3D getOrbitCenter() {
		return new Vector3D(orbitCenter);
	}

	/**
	 * Sets the projection mode to perspective projection with a default field
	 * of view. In this mode, closer objects appear larger, as in real life.
	 */
	public static void setPerspectiveProjection() {
		setPerspectiveProjection(DEFAULT_FOV);
	}

	/**
	 * Sets the projection mode to perspective projection with the given field
	 * of view. In this mode, closer objects appear larger, as in real life. A
	 * larger field of view means that there is more perspective distortion.
	 * Reasonable values are between 0.5 and 3.0.
	 * 
	 * @param fov
	 *            The field of view to use. Try [0.5-3.0].
	 */
	public static void setPerspectiveProjection(double fov) {

		view.setProjectionPolicy(View.PERSPECTIVE_PROJECTION);
		view.setWindowEyepointPolicy(View.RELATIVE_TO_FIELD_OF_VIEW);
		view.setFieldOfView(fov);
		setScreenScale(1);
		orbit.setZoomEnable(true);
		perspectiveButton.setSelected(true);
		if ((Double) fovSpinner.getValue() != fov)
			fovSpinner.setValue(fov);
		// if (view.getProjectionPolicy() == View.PERSPECTIVE_PROJECTION)
		// return;
	}

	/**
	 * Sets the projection mode to orthographic projection. In this mode,
	 * parallel lines remain parallel after projection, and there is no
	 * perspective. It is as looking from infinitely far away with a telescope.
	 * AutoCAD programs use this projection mode.
	 */
	public static void setParallelProjection() {

		if (view.getProjectionPolicy() == View.PARALLEL_PROJECTION)
			return;
		view.setProjectionPolicy(View.PARALLEL_PROJECTION);
		orbit.setZoomEnable(false);
		parallelButton.setSelected(true);

		setScreenScale(0.3 / zoom);
	}

	private static void setScreenScale(double scale) {
		double ratio = scale / view.getScreenScale();
		view.setScreenScale(scale);
		view.setFrontClipDistance(view.getFrontClipDistance() * ratio);
		view.setBackClipDistance(view.getBackClipDistance() * ratio);
	}

	/* ***************************************************************
	 * Mouse Listener Methods *
	 * ***************************************************************
	 */

	/** Is any mouse button pressed? */
	public static boolean mousePressed() {
		synchronized (mouseLock) {
			return (mouse1Pressed() || mouse2Pressed() || mouse3Pressed());
		}
	}

	/**
	 * Is the mouse1 button pressed down?
	 */
	public static boolean mouse1Pressed() {
		synchronized (mouseLock) {
			return mouse1;
		}
	}

	/**
	 * Is the mouse2 button pressed down?
	 */
	public static boolean mouse2Pressed() {
		synchronized (mouseLock) {
			return mouse2;
		}
	}

	/**
	 * Is the mouse3 button pressed down?
	 */
	public static boolean mouse3Pressed() {
		synchronized (mouseLock) {
			return mouse3;
		}
	}

	/**
	 * Where is the mouse?
	 * 
	 * @return The value of the X-coordinate of the mouse.
	 */
	public static double mouseX() {
		synchronized (mouseLock) {
			return unscaleX(mouseX);
		}
	}

	/**
	 * Where is the mouse?
	 * 
	 * @return The value of the Y-coordinate of the mouse.
	 */
	public static double mouseY() {
		synchronized (mouseLock) {
			return unscaleY(mouseY);
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mouseClicked(MouseEvent e) {
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mouseEntered(MouseEvent e) {
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mouseExited(MouseEvent e) {

	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mousePressed(MouseEvent e) {
		synchronized (mouseLock) {
			mouseX = e.getX();
			mouseY = e.getY();
			if (e.getButton() == 1)
				mouse1 = true;
			if (e.getButton() == 2)
				mouse2 = true;
			if (e.getButton() == 3)
				mouse3 = true;
			// System.out.println("Mouse button = " + e.getButton());
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mouseReleased(MouseEvent e) {
		synchronized (mouseLock) {
			if (e.getButton() == 1)
				mouse1 = false;
			if (e.getButton() == 2)
				mouse2 = false;
			if (e.getButton() == 3)
				mouse3 = false;
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mouseDragged(MouseEvent e) {
		synchronized (mouseLock) {

			mouseMotionEvents(e, e.getX(), e.getY(), true);
			mouseX = e.getX();
			mouseY = e.getY();
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mouseMoved(MouseEvent e) {
		synchronized (mouseLock) {

			// System.out.println(e.getX() + " " + e.getY());
			mouseMotionEvents(e, e.getX(), e.getY(), false);
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void mouseWheelMoved(MouseWheelEvent e) {

		double notches = e.getWheelRotation();
		// System.out.println(notches);
		if ((cameraMode == ORBIT_MODE) && (view.getProjectionPolicy() == View.PARALLEL_PROJECTION)) {
			camera.moveRelative(0, 0, notches * zoom / 20);
		}
	}

	private static void mouseMotionEvents(MouseEvent e, double newX, double newY, boolean dragged) {

		// System.out.println("x = " + mouseX() + " y = " + mouseY());

		if (cameraMode == FIXED_MODE)
			return;

		if (cameraMode == FPS_MODE) {
			if (dragged || immersive) {
				camera.rotateFPS((mouseY - newY) / 4, (mouseX - newX) / 4, 0);
			}
			return;
		}

		if ((cameraMode == AIRPLANE_MODE)) {
			if (dragged || immersive)
				camera.rotateRelative((mouseY - newY) / 4, (mouseX - newX) / 4, 0);
			return;
		}

		if ((cameraMode == LOOK_MODE)) {
			if (dragged || immersive)
				camera.rotateFPS((mouseY - newY) / 4, (mouseX - newX) / 4, 0);
			return;
		}

		if ((cameraMode == ORBIT_MODE) && (dragged && isKeyPressed(KeyEvent.VK_ALT))
				&& (view.getProjectionPolicy() == View.PARALLEL_PROJECTION)) {
			camera.moveRelative(0, 0, (double) (newY - mouseY) * zoom / 50);
			return;
		}
	}

	/* ***************************************************************
	 * Keyboard Listener Methods *
	 * ***************************************************************
	 */

	/**
	 * Has the user typed a key?
	 * 
	 * @return True if the user has typed a key, false otherwise.
	 */
	public static boolean hasNextKeyTyped() {
		synchronized (keyLock) {
			return !keysTyped.isEmpty();
		}
	}

	/**
	 * What is the next key that was typed by the user?
	 * 
	 * @return The next key typed.
	 */
	public static char nextKeyTyped() {
		synchronized (keyLock) {
			return keysTyped.removeLast();
		}
	}

	/**
	 * Is the given key currently pressed down? The keys correnspond to physical
	 * keys, not characters. For letters, use the uppercase character to refer
	 * to the key. For arrow keys and modifiers such as shift and ctrl, refer to
	 * the KeyEvent constants, such as KeyEvent.VK_SHIFT.
	 * 
	 * @param key
	 *            The given key
	 * @return True if the given character is pressed.
	 */
	public static boolean isKeyPressed(int key) {
		synchronized (keyLock) {
			return keysDown.contains(key);
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void keyTyped(KeyEvent e) {
		synchronized (keyLock) {

			char c = e.getKeyChar();
			keysTyped.addFirst(c);

			if (c == '`')
				setCameraMode((getCameraMode() + 1) % 5);
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void keyPressed(KeyEvent e) {
		synchronized (keyLock) {
			keysDown.add(e.getKeyCode());
			// System.out.println((int)e.getKeyCode() + " pressed");
		}
	}

	/**
	 * This method cannot be called directly.
	 * 
	 * @deprecated
	 */
	public void keyReleased(KeyEvent e) {
		synchronized (keyLock) {
			keysDown.remove(e.getKeyCode());
			// System.out.println((int)e.getKeyCode() + " released");
		}
	}

	/**
	 * Processes one frame of keyboard events.
	 * 
	 * @param time
	 *            The amount of time for this frame.
	 */
	private static void moveEvents(int time) {

		infoDisplay();

		if (isKeyPressed(KeyEvent.VK_CONTROL))
			return;

		if (cameraMode == FPS_MODE) {
			double move = 0.00015 * time * (zoom);
			if (isKeyPressed('W') || isKeyPressed(KeyEvent.VK_UP))
				camera.moveRelative(0, 0, move * 3);
			if (isKeyPressed('S') || isKeyPressed(KeyEvent.VK_DOWN))
				camera.moveRelative(0, 0, -move * 3);
			if (isKeyPressed('A') || isKeyPressed(KeyEvent.VK_LEFT))
				camera.moveRelative(-move, 0, 0);
			if (isKeyPressed('D') || isKeyPressed(KeyEvent.VK_RIGHT))
				camera.moveRelative(move, 0, 0);
			if (isKeyPressed('Q') || isKeyPressed(KeyEvent.VK_PAGE_UP))
				camera.moveRelative(0, move, 0);
			if (isKeyPressed('E') || isKeyPressed(KeyEvent.VK_PAGE_DOWN))
				camera.moveRelative(0, -move, 0);
		}
		if (cameraMode == AIRPLANE_MODE) {
			double move = 0.00015 * time * (zoom);
			if (isKeyPressed('W') || isKeyPressed(KeyEvent.VK_UP))
				camera.moveRelative(0, 0, move * 3);
			if (isKeyPressed('S') || isKeyPressed(KeyEvent.VK_DOWN))
				camera.moveRelative(0, 0, -move * 3);
			if (isKeyPressed('A') || isKeyPressed(KeyEvent.VK_LEFT))
				camera.moveRelative(-move, 0, 0);
			if (isKeyPressed('D') || isKeyPressed(KeyEvent.VK_RIGHT))
				camera.moveRelative(move, 0, 0);
			if (isKeyPressed('Q') || isKeyPressed(KeyEvent.VK_PAGE_UP))
				camera.rotateRelative(0, 0, move * 250 / zoom);
			if (isKeyPressed('E') || isKeyPressed(KeyEvent.VK_PAGE_DOWN))
				camera.rotateRelative(0, 0, -move * 250 / zoom);
		}
	}

	/* ***************************************************************
	 * Action Listener Methods *
	 * ***************************************************************
	 */

	private static void save3DAction() {

		FileDialog chooser = new FileDialog(StdDraw3D.frame, "Save as a 3D file for loading later.", FileDialog.SAVE);
		chooser.setVisible(true);
		String filename = chooser.getFile();
		if (filename != null) {
			StdDraw3D.saveScene3D(chooser.getDirectory() + File.separator + chooser.getFile());
		}

		keysDown.remove(KeyEvent.VK_META);
		keysDown.remove(KeyEvent.VK_CONTROL);
		keysDown.remove(KeyEvent.VK_E);
	}

	private static void loadAction() {

		FileDialog chooser = new FileDialog(frame, "Pick a .obj or .ply file to load.", FileDialog.LOAD);
		chooser.setVisible(true);
		String filename = chooser.getDirectory() + chooser.getFile();
		model(filename);

		keysDown.remove(KeyEvent.VK_META);
		keysDown.remove(KeyEvent.VK_CONTROL);
		keysDown.remove(KeyEvent.VK_L);
	}

	private static void saveAction() {

		FileDialog chooser = new FileDialog(frame, "Use a .png or .jpg extension.", FileDialog.SAVE);
		chooser.setVisible(true);
		String filename = chooser.getFile();

		if (filename != null) {
			StdDraw3D.save(chooser.getDirectory() + File.separator + chooser.getFile());
		}

		keysDown.remove(KeyEvent.VK_META);
		keysDown.remove(KeyEvent.VK_CONTROL);
		keysDown.remove(KeyEvent.VK_S);
	}

	private static void quitAction() {

		WindowEvent wev = new WindowEvent(frame, WindowEvent.WINDOW_CLOSING);
		Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(wev);

		keysDown.remove(KeyEvent.VK_META);
		keysDown.remove(KeyEvent.VK_CONTROL);
		keysDown.remove(KeyEvent.VK_Q);
	}

	/* ***************************************************************
	 * Light and Background Methods *
	 * ***************************************************************
	 */

	/**
	 * Sets the background color to the given color.
	 * 
	 * @param color
	 *            The color to set the background as.
	 */
	public static void setBackground(Color color) {

		if (!color.equals(bgColor)) {
			bgColor = color;

			rootGroup.removeChild(bgGroup);
			bgGroup.removeChild(background);

			background = createBackground();
			background.setColor(new Color3f(bgColor));

			bgGroup.addChild(background);
			rootGroup.addChild(bgGroup);
		}
	}

	/**
	 * Sets the background image to the given filename, scaled to fit the
	 * window.
	 * 
	 * @param imageURL
	 *            The filename for the background image.
	 */
	public static void setBackground(String imageURL) {

		rootGroup.removeChild(bgGroup);
		bgGroup.removeChild(background);

		background = createBackground();

		BufferedImage bi = null;
		try {
			bi = ImageIO.read(new File(imageURL));
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}

		if (bi == null) {
			try {
				ImageIO.read(new URL(imageURL));
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		ImageComponent2D imageComp = new ImageComponent2D(ImageComponent.FORMAT_RGB, bi);
		background.setImage(imageComp);
		background.setImageScaleMode(Background.SCALE_FIT_ALL);

		bgGroup.addChild(background);
		rootGroup.addChild(bgGroup);
	}

	/**
	 * Sets the background to the given image file. The file gets wrapped around
	 * as a spherical skybox.
	 * 
	 * @param imageURL
	 *            The background image to use.
	 */
	public static void setBackgroundSphere(String imageURL) {

		Sphere sphere = new Sphere(1.1f, Sphere.GENERATE_NORMALS | Sphere.GENERATE_NORMALS_INWARD
				| Sphere.GENERATE_TEXTURE_COORDS, numDivisions);

		Appearance ap = sphere.getAppearance();

		Texture texture = createTexture(imageURL);

		TextureAttributes texAttr = new TextureAttributes();
		texAttr.setTextureMode(TextureAttributes.REPLACE);

		ap.setTexture(texture);
		ap.setTextureAttributes(texAttr);

		sphere.setAppearance(ap);

		BranchGroup backGeoBranch = createBranchGroup();
		backGeoBranch.addChild(sphere);

		rootGroup.removeChild(bgGroup);
		bgGroup.removeChild(background);

		background = createBackground();
		background.setGeometry(backGeoBranch);

		bgGroup.addChild(background);
		rootGroup.addChild(bgGroup);

	}

	// *********************************************************************************************

	public static void clearSound() {
		soundGroup.removeAllChildren();
	}

	public static void playAmbientSound(String filename) {
		playAmbientSound(filename, 1.0, false);
	}

	public static void playAmbientSound(String filename, boolean loop) {
		playAmbientSound(filename, 1.0, loop);
	}

	private static void playAmbientSound(String filename, double volume, boolean loop) {

		MediaContainer mc = new MediaContainer("file:" + filename);
		mc.setCacheEnable(true);

		BackgroundSound sound = new BackgroundSound();
		sound.setInitialGain((float) volume);
		sound.setSoundData(mc);
		sound.setBounds(INFINITE_BOUNDS);
		sound.setSchedulingBounds(INFINITE_BOUNDS);
		if (loop == true)
			sound.setLoop(Sound.INFINITE_LOOPS);
		sound.setEnable(true);

		BranchGroup bg = createBranchGroup();
		bg.addChild(sound);
		soundGroup.addChild(bg);
	}

	// public static void playPointSound (String filename, Vector3D position,
	// double volume, boolean loop) {
	//
	// MediaContainer mc = new MediaContainer("file:" + filename);
	// mc.setCacheEnable(true);
	//
	// PointSound sound = new PointSound();
	// sound.setInitialGain((float)volume);
	// sound.setSoundData(mc);
	// sound.setBounds(INFINITE_BOUNDS);
	// sound.setSchedulingBounds(INFINITE_BOUNDS);
	// if (loop == true) sound.setLoop(Sound.INFINITE_LOOPS);
	// sound.setPosition(createPoint3f(position));
	// Point2f[] distanceGain = new Point2f[2];
	// distanceGain[0] = new Point2f(0, 1);
	// distanceGain[1] = new Point2f(20, 0);
	// sound.setDistanceGain(distanceGain);
	// sound.setEnable(true);
	//
	// BranchGroup bg = createBranchGroup();
	// bg.addChild(sound);
	// soundGroup.addChild(bg);
	// }
	// *********************************************************************************************

	public static void clearFog() {
		fogGroup.removeAllChildren();
	}

	public static void addFog(Color col, double frontDistance, double backDistance) {
		LinearFog fog = new LinearFog(new Color3f(col), frontDistance, backDistance);
		fog.setInfluencingBounds(INFINITE_BOUNDS);

		BranchGroup bg = createBranchGroup();
		bg.addChild(fog);
		fogGroup.addChild(bg);
	}

	// *********************************************************************************************

	/**
	 * Removes all current lighting from the scene.
	 */
	public static void clearLight() {
		lightGroup.removeAllChildren();
	}

	/**
	 * Adds the default lighting to the scene, called automatically at startup.
	 */
	public static void setDefaultLight() {
		clearLight();
		directionalLight(-4f, 7f, 12f, LIGHT_GRAY);
		directionalLight(4f, -7f, -12f, WHITE);
		ambientLight(new Color(0.1f, 0.1f, 0.1f));
	}

	/**
	 * Adds a directional light of color col which appears to come from (x, y,
	 * z).
	 */
	public static Light directionalLight(Vector3D dir, Color col) {
		return directionalLight(dir.x, dir.y, dir.z, col);
	}

	/**
	 * Adds a directional light of color col which shines in the direction
	 * vector (x, y, z)
	 */
	public static Light directionalLight(double x, double y, double z, Color col) {

		DirectionalLight light = new DirectionalLight();
		light.setColor(new Color3f(col));

		light.setInfluencingBounds(INFINITE_BOUNDS);
		light.setCapability(DirectionalLight.ALLOW_STATE_WRITE);
		light.setCapability(DirectionalLight.ALLOW_COLOR_WRITE);
		light.setEnable(true);

		BranchGroup bg = createBranchGroup();
		TransformGroup tg = createTransformGroup();
		tg.addChild(light);
		bg.addChild(tg);
		lightGroup.addChild(bg);

		Light l = new Light(bg, tg, light);
		l.setDirection(new Vector3D(x, y, z));
		return l;
	}

	/**
	 * Adds ambient light of color col.
	 */
	public static Light ambientLight(Color col) {

		Color3f lightColor = new Color3f(col);
		AmbientLight light = new AmbientLight(lightColor);

		light.setInfluencingBounds(INFINITE_BOUNDS);
		light.setCapability(AmbientLight.ALLOW_STATE_WRITE);
		light.setCapability(AmbientLight.ALLOW_COLOR_WRITE);

		BranchGroup bg = createBranchGroup();
		TransformGroup tg = createTransformGroup();
		tg.addChild(light);
		bg.addChild(tg);
		lightGroup.addChild(bg);

		return new Light(bg, tg, light);
	}

	public static Light pointLight(Vector3D origin, Color col) {
		return pointLight(origin.x, origin.y, origin.z, col, 1.0);
	}

	public static Light pointLight(double x, double y, double z, Color col) {
		return pointLight(x, y, z, col, 1.0);
	}

	public static Light pointLight(Vector3D origin, Color col, double power) {
		return pointLight(origin.x, origin.y, origin.z, col, power);
	}

	public static Light pointLight(double x, double y, double z, Color col, double power) {

		PointLight light = new PointLight();
		light.setColor(new Color3f(col));

		light.setInfluencingBounds(INFINITE_BOUNDS);
		light.setCapability(PointLight.ALLOW_STATE_WRITE);
		light.setCapability(PointLight.ALLOW_COLOR_WRITE);
		light.setCapability(PointLight.ALLOW_ATTENUATION_WRITE);

		float scale = (float) zoom;
		float linearFade = 0.03f;
		float quadraticFade = 0.03f;
		light.setAttenuation(1.0f, linearFade / scale, quadraticFade / (scale * scale));

		BranchGroup bg = createBranchGroup();
		TransformGroup tg = createTransformGroup();
		tg.addChild(light);
		bg.addChild(tg);
		lightGroup.addChild(bg);

		Light l = new Light(bg, tg, light);
		l.setPosition(x, y, z);
		l.scalePower(power);

		return l;
	}

	/**
	 * Returns a randomly generated color.
	 */
	public static Color randomColor() {
		return new Color(new Random().nextInt());
	}

	/**
	 * Returns a randomly generated color on the rainbow.
	 */
	public static Color randomRainbowColor() {
		return Color.getHSBColor((float) Math.random(), 1.0f, 1.0f);
	}

	/**
	 * Returns a randomly generated normalized Vector3D.
	 */
	public static Vector3D randomDirection() {

		double theta = Math.random() * Math.PI * 2;
		double phi = Math.random() * Math.PI;
		return new Vector3D(Math.cos(theta) * Math.sin(phi), Math.sin(theta) * Math.sin(phi), Math.cos(phi));
	}

	/* ***************************************************************
	 * Animation Frame Methods *
	 * ***************************************************************
	 */

	/**
	 * Clears the entire on the next call of show().
	 */
	public static void clear() {
		clear3D();
		clearOverlay();
	}

	public static void clear(Color color) {
		setBackground(color);
		clear();
	}

	/**
	 * Clears the 3D world on the screen for the next call of show();
	 */
	public static void clear3D() {
		clear3D = true;
		offscreenGroup = createBranchGroup();
	}

	/**
	 * Clears the 2D overlay for the next call of show();
	 */
	public static void clearOverlay() {
		clearOverlay = true;
		offscreenImage = createBufferedImage();
	}

	/**
	 * Pauses for a given amount of milliseconds.
	 * 
	 * @param time
	 *            The number of milliseconds to pause for.
	 */
	public static void pause(int time) {

		int t = time;
		int dt = 15;

		while (t > dt) {
			moveEvents(dt);
			Toolkit.getDefaultToolkit().sync();
			try {
				TimeUnit.MILLISECONDS.sleep(t);
			} catch (InterruptedException e) {
				System.out.println("Error sleeping");
			}

			t -= dt;
		}

		moveEvents(t);
		if (t == 0)
			return;
		try {
			TimeUnit.MILLISECONDS.sleep(t);
		} catch (InterruptedException e) {
			System.out.println("Error sleeping");
		}

		// while (!renderedOnce) {
		// try { Thread.currentThread().sleep(1); }
		// catch (InterruptedException e) {
		// System.out.println("Error sleeping"); }
		//
		// renderedOnce = false;
		// showedOnce = true;
	}

	/**
	 * Allows for camera navigation of a scene without redrawing. Useful when
	 * drawing a complicated scene once and then exploring without redrawing.
	 * Call only as the last line of a program.
	 */
	public static void finished() {

		show(1000000000);
	}

	/**
	 * Renders to the screen, but does not pause.
	 */
	public static void show() {
		show(0);
	}

	/**
	 * Renders drawn 3D objects to the screen and draws the 2D overlay, then
	 * pauses for the given time.
	 * 
	 * @param time
	 *            The number of milliseconds to pause for.
	 */
	public static void show(int time) {

		renderOverlay();
		render3D();
		pause(time);
	}

	public static void showOverlay() {
		showOverlay(0);
	}

	/**
	 * Displays the drawn overlay onto the screen.
	 */
	public static void showOverlay(int time) {

		renderOverlay();
		pause(time);
	}

	private static void renderOverlay() {
		if (clearOverlay) {
			clearOverlay = false;
			onscreenImage = offscreenImage;
		} else {
			Graphics2D graphics = (Graphics2D) onscreenImage.getGraphics();
			graphics.drawRenderedImage(offscreenImage, new AffineTransform());
		}
		offscreenImage = createBufferedImage();
	}

	public static void show3D() {
		show3D(0);
	}

	/**
	 * Renders the drawn 3D objects to the screen, but not the overlay.
	 */
	public static void show3D(int time) {

		render3D();
		pause(time);
	}

	@SuppressWarnings("rawtypes")
	private static void render3D() {

		rootGroup.addChild(offscreenGroup);

		if (clear3D) {
			clear3D = false;
			rootGroup.removeChild(onscreenGroup);
			onscreenGroup = offscreenGroup;
		} else {
			Enumeration children = offscreenGroup.getAllChildren();
			while (children.hasMoreElements()) {
				Node child = (Node) children.nextElement();
				offscreenGroup.removeChild(child);
				onscreenGroup.addChild(child);
			}
		}

		offscreenGroup = createBranchGroup();
		// System.out.println("off = " + offscreenGroup.numChildren());
		// System.out.println("on  = " + onscreenGroup.numChildren());

		rootGroup.removeChild(offscreenGroup);

		// StdOut.println("showed once = true");
	}

	/* ***************************************************************
	 * Primitive 3D Drawing Methods *
	 * ***************************************************************
	 */

	/**
	 * Draws a sphere at (x, y, z) with radius r.
	 */
	public static Shape sphere(double x, double y, double z, double r) {
		return sphere(x, y, z, r, 0, 0, 0, null);
	}

	/**
	 * Draws a sphere at (x, y, z) with radius r and axial rotations (xA, yA,
	 * zA).
	 */
	public static Shape sphere(double x, double y, double z, double r, double xA, double yA, double zA) {
		return sphere(x, y, z, r, xA, yA, zA, null);
	}

	/**
	 * Draws a sphere at (x, y, z) with radius r and a texture from imageURL.
	 */
	public static Shape sphere(double x, double y, double z, double r, String imageURL) {
		return sphere(x, y, z, r, 0, 0, 0, imageURL);
	}

	/**
	 * Draws a sphere at (x, y, z) with radius r, axial rotations (xA, yA, zA),
	 * and a texture from imageURL.
	 */
	public static Shape sphere(double x, double y, double z, double r, double xA, double yA, double zA, String imageURL) {

		Vector3f dimensions = createVector3f(0, 0, r);
		Sphere sphere = new Sphere(dimensions.z, PRIMFLAGS, numDivisions);
		sphere.setAppearance(createAppearance(imageURL, true));
		return primitive(sphere, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	/**
	 * Draws a wireframe sphere at (x, y, z) with radius r.
	 */
	public static Shape wireSphere(double x, double y, double z, double r) {
		return wireSphere(x, y, z, r, 0, 0, 0);
	}

	/**
	 * Draws a wireframe sphere at (x, y, z) with radius r and axial rotations
	 * (xA, yA, zA).
	 */
	public static Shape wireSphere(double x, double y, double z, double r, double xA, double yA, double zA) {

		Vector3f dimensions = createVector3f(0, 0, r);
		Sphere sphere = new Sphere(dimensions.z, PRIMFLAGS, numDivisions);
		sphere.setAppearance(createAppearance(null, false));
		return primitive(sphere, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	// *********************************************************************************************

	/**
	 * Draws an ellipsoid at (x, y, z) with dimensions (w, h, d).
	 */
	public static Shape ellipsoid(double x, double y, double z, double w, double h, double d) {
		return ellipsoid(x, y, z, w, h, d, 0, 0, 0, null);
	}

	/**
	 * Draws an ellipsoid at (x, y, z) with dimensions (w, h, d) and axial
	 * rotations (xA, yA, zA).
	 */
	public static Shape ellipsoid(double x, double y, double z, double w, double h, double d, double xA, double yA,
			double zA) {
		return ellipsoid(x, y, z, w, h, d, xA, yA, zA, null);
	}

	/**
	 * Draws an ellipsoid at (x, y, z) with dimensions (w, h, d) and a texture
	 * from imageURL.
	 */
	public static Shape ellipsoid(double x, double y, double z, double w, double h, double d, String imageURL) {
		return ellipsoid(x, y, z, w, h, d, 0, 0, 0, imageURL);
	}

	/**
	 * Draws an ellipsoid at (x, y, z) with dimensions (w, h, d), axial
	 * rotations (xA, yA, zA), and a texture from imageURL.
	 */
	public static Shape ellipsoid(double x, double y, double z, double w, double h, double d, double xA, double yA,
			double zA, String imageURL) {

		Sphere sphere = new Sphere(1, PRIMFLAGS, numDivisions);
		sphere.setAppearance(createAppearance(imageURL, true));
		return primitive(sphere, x, y, z, new Vector3d(xA, yA, zA), new Vector3d(w, h, d));
	}

	/**
	 * Draws a wireframe ellipsoid at (x, y, z) with dimensions (w, h, d).
	 */
	public static Shape wireEllipsoid(double x, double y, double z, double w, double h, double d) {
		return wireEllipsoid(x, y, z, w, h, d, 0, 0, 0);
	}

	/**
	 * Draws a wireframe ellipsoid at (x, y, z) with dimensions (w, h, d) and
	 * axial rotations (xA, yA, zA).
	 */
	public static Shape wireEllipsoid(double x, double y, double z, double w, double h, double d, double xA, double yA,
			double zA) {

		Sphere sphere = new Sphere(1, PRIMFLAGS, numDivisions);
		sphere.setAppearance(createAppearance(null, false));
		return primitive(sphere, x, y, z, new Vector3d(xA, yA, zA), new Vector3d(w, h, d));
	}

	// *********************************************************************************************
	/**
	 * Draws a cube at (x, y, z) with radius r.
	 */
	public static Shape cube(double x, double y, double z, double r) {
		return cube(x, y, z, r, 0, 0, 0, null);
	}

	/**
	 * Draws a cube at (x, y, z) with radius r and axial rotations (xA, yA, zA).
	 */
	public static Shape cube(double x, double y, double z, double r, double xA, double yA, double zA) {
		return cube(x, y, z, r, xA, yA, zA, null);
	}

	/**
	 * Draws a cube at (x, y, z) with radius r and a texture from imageURL.
	 */
	public static Shape cube(double x, double y, double z, double r, String imageURL) {
		return cube(x, y, z, r, 0, 0, 0, imageURL);
	}

	/**
	 * Draws a cube at (x, y, z) with radius r, axial rotations (xA, yA, zA),
	 * and a texture from imageURL.
	 */
	public static Shape cube(double x, double y, double z, double r, double xA, double yA, double zA, String imageURL) {
		return box(x, y, z, r, r, r, xA, yA, zA, imageURL);
	}

	/**
	 * Draws a wireframe cube at (x, y, z) with radius r and axial rotations
	 * (xA, yA, zA).
	 */
	public static Shape wireCube(double x, double y, double z, double r, double xA, double yA, double zA) {

		return wireBox(x, y, z, r, r, r, 0, 0, 0);
	}

	/**
	 * Draws a wireframe cube at (x, y, z) with radius r.
	 */
	public static Shape wireCube(double x, double y, double z, double r) {

		double[] xC = new double[] { x + r, x + r, x - r, x - r, x + r, x + r, x + r, x - r, x - r, x + r, x + r,
				x + r, x - r, x - r, x - r, x - r };
		double[] yC = new double[] { y + r, y - r, y - r, y + r, y + r, y + r, y - r, y - r, y + r, y + r, y - r,
				y - r, y - r, y - r, y + r, y + r };
		double[] zC = new double[] { z + r, z + r, z + r, z + r, z + r, z - r, z - r, z - r, z - r, z - r, z - r,
				z + r, z + r, z - r, z - r, z + r };

		return lines(xC, yC, zC);
	}

	// *********************************************************************************************

	/**
	 * Draws a box at (x, y, z) with dimensions (w, h, d).
	 */
	public static Shape box(double x, double y, double z, double w, double h, double d) {
		return box(x, y, z, w, h, d, 0, 0, 0, null);
	}

	/**
	 * Draws a box at (x, y, z) with dimensions (w, h, d) and axial rotations
	 * (xA, yA, zA).
	 */
	public static Shape box(double x, double y, double z, double w, double h, double d, double xA, double yA, double zA) {
		return box(x, y, z, w, h, d, xA, yA, zA, null);
	}

	/**
	 * Draws a box at (x, y, z) with dimensions (w, h, d) and a texture from
	 * imageURL.
	 */
	public static Shape box(double x, double y, double z, double w, double h, double d, String imageURL) {
		return box(x, y, z, w, h, d, 0, 0, 0, imageURL);
	}

	/**
	 * Draws a box at (x, y, z) with dimensions (w, h, d), axial rotations (xA,
	 * yA, zA), and a texture from imageURL.
	 */
	public static Shape box(double x, double y, double z, double w, double h, double d, double xA, double yA,
			double zA, String imageURL) {

		Appearance ap = createAppearance(imageURL, true);

		Vector3f dimensions = createVector3f(w, h, d);

		com.sun.j3d.utils.geometry.Box box = new com.sun.j3d.utils.geometry.Box(dimensions.x, dimensions.y,
				dimensions.z, PRIMFLAGS, ap, numDivisions);
		return primitive(box, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	/**
	 * Draws a wireframe box at (x, y, z) with dimensions (w, h, d).
	 */
	public static Shape wireBox(double x, double y, double z, double w, double h, double d) {
		return wireBox(x, y, z, w, h, d, 0, 0, 0);
	}

	/**
	 * Draws a wireframe box at (x, y, z) with dimensions (w, h, d) and axial
	 * rotations (xA, yA, zA).
	 */
	public static Shape wireBox(double x, double y, double z, double w, double h, double d, double xA, double yA,
			double zA) {

		Appearance ap = createAppearance(null, false);

		Vector3f dimensions = createVector3f(w, h, d);

		com.sun.j3d.utils.geometry.Box box = new com.sun.j3d.utils.geometry.Box(dimensions.x, dimensions.y,
				dimensions.z, PRIMFLAGS, ap, numDivisions);
		return primitive(box, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	// *********************************************************************************************

	/**
	 * Draws a cylinder at (x, y, z) with radius r and height h.
	 */
	public static Shape cylinder(double x, double y, double z, double r, double h) {
		return cylinder(x, y, z, r, h, 0, 0, 0, null);
	}

	/**
	 * Draws a cylinder at (x, y, z) with radius r, height h, and axial
	 * rotations (xA, yA, zA).
	 */
	public static Shape cylinder(double x, double y, double z, double r, double h, double xA, double yA, double zA) {
		return cylinder(x, y, z, r, h, xA, yA, zA, null);
	}

	/**
	 * Draws a cylinder at (x, y, z) with radius r, height h, and a texture from
	 * imageURL.
	 */
	public static Shape cylinder(double x, double y, double z, double r, double h, String imageURL) {
		return cylinder(x, y, z, r, h, 0, 0, 0, imageURL);
	}

	/**
	 * Draws a cylinder at (x, y, z) with radius r, height h, axial rotations
	 * (xA, yA, zA), and a texture from imageURL.
	 */
	public static Shape cylinder(double x, double y, double z, double r, double h, double xA, double yA, double zA,
			String imageURL) {

		Appearance ap = createAppearance(imageURL, true);
		Vector3f dimensions = createVector3f(r, h, 0);
		Cylinder cyl = new Cylinder(dimensions.x, dimensions.y, PRIMFLAGS, numDivisions, numDivisions, ap);
		return primitive(cyl, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	/**
	 * Draws a wireframe cylinder at (x, y, z) with radius r and height h.
	 */
	public static Shape wireCylinder(double x, double y, double z, double r, double h) {
		return wireCylinder(x, y, z, r, h, 0, 0, 0);
	}

	/**
	 * Draws a wireframe cylinder at (x, y, z) with radius r, height h, and
	 * axial rotations (xA, yA, zA).
	 */
	public static Shape wireCylinder(double x, double y, double z, double r, double h, double xA, double yA, double zA) {

		Appearance ap = createAppearance(null, false);
		Vector3f dimensions = createVector3f(r, h, 0);
		Cylinder cyl = new Cylinder(dimensions.x, dimensions.y, PRIMFLAGS, numDivisions, numDivisions, ap);
		return primitive(cyl, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	// *********************************************************************************************

	/**
	 * Draws a cone at (x, y, z) with radius r and height h.
	 */
	public static Shape cone(double x, double y, double z, double r, double h) {
		return cone(x, y, z, r, h, 0, 0, 0, null);
	}

	/**
	 * Draws a cone at (x, y, z) with radius r, height h, and axial rotations
	 * (xA, yA, zA).
	 */
	public static Shape cone(double x, double y, double z, double r, double h, double xA, double yA, double zA) {
		return cone(x, y, z, r, h, xA, yA, zA, null);
	}

	/**
	 * Draws a cone at (x, y, z) with radius r, height h, and a texture from
	 * imageURL.
	 */
	public static Shape cone(double x, double y, double z, double r, double h, String imageURL) {
		return cone(x, y, z, r, h, 0, 0, 0, imageURL);
	}

	/**
	 * Draws a cone at (x, y, z) with radius r, height h, axial rotations (xA,
	 * yA, zA), and a texture from imageURL.
	 */
	public static Shape cone(double x, double y, double z, double r, double h, double xA, double yA, double zA,
			String imageURL) {

		Appearance ap = createAppearance(imageURL, true);
		Vector3f dimensions = createVector3f(r, h, 0);
		Cone cone = new Cone(dimensions.x, dimensions.y, PRIMFLAGS, numDivisions, numDivisions, ap);
		return primitive(cone, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	/**
	 * Draws a wireframe cone at (x, y, z) with radius r and height h.
	 */
	public static Shape wireCone(double x, double y, double z, double r, double h) {
		return wireCone(x, y, z, r, h, 0, 0, 0);
	}

	/**
	 * Draws a wireframe cone at (x, y, z) with radius r, height h, and axial
	 * rotations (xA, yA, zA).
	 */
	public static Shape wireCone(double x, double y, double z, double r, double h, double xA, double yA, double zA) {

		Appearance ap = createAppearance(null, false);
		Vector3f dimensions = createVector3f(r, h, 0);
		Cone cone = new Cone(dimensions.x, dimensions.y, PRIMFLAGS, numDivisions, numDivisions, ap);
		return primitive(cone, x, y, z, new Vector3d(xA, yA, zA), null);
	}

	// *********************************************************************************************

	/**
	 * Draws a Java 3D Primitive object at (x, y, z) with axial rotations (xA,
	 * yA, zA).
	 */
	private static Shape primitive(Primitive shape, double x, double y, double z, Vector3d angles, Vector3d scales) {

		shape.setCapability(Primitive.ENABLE_APPEARANCE_MODIFY);
		shape.setPickable(false);
		shape.setCollidable(false);

		TransformGroup tgScale = createTransformGroup();
		Transform3D scaleTransform = new Transform3D();
		if (scales != null)
			scaleTransform.setScale(scales);
		tgScale.setTransform(scaleTransform);
		tgScale.addChild(shape);

		TransformGroup tgShape = createTransformGroup();
		Transform3D transform = new Transform3D();
		if (angles != null) {
			angles.scale(Math.PI / 180);
			transform.setEuler(angles);
		}
		Vector3f vector = createVector3f(x, y, z);
		transform.setTranslation(vector);
		tgShape.setTransform(transform);
		tgShape.addChild(tgScale);

		BranchGroup bg = createBranchGroup();
		bg.addChild(tgShape);
		offscreenGroup.addChild(bg);

		return new Shape(bg, tgShape);
	}

	/* ***************************************************************
	 * Non-Primitive Drawing Methods *
	 * ***************************************************************
	 */

	/**
	 * Draws a single point at (x, y, z).
	 */
	public static Shape point(double x, double y, double z) {

		return points(new double[] { x }, new double[] { y }, new double[] { z });
	}

	/**
	 * Draws a set of points at the given coordinates. For example, the first
	 * point is at (x[0], y[0], z[0]). Much more efficient than drawing
	 * individual points.
	 */
	public static Shape points(double[] x, double[] y, double[] z) {

		Point3f[] coords = constructPoint3f(x, y, z);

		GeometryArray geom = new PointArray(coords.length, PointArray.COORDINATES);
		geom.setCoordinates(0, coords);

		Shape3D shape = createShape3D(geom);

		return shape(shape);
	}

	/**
	 * Draws a set of points at the given coordinates with the given colors. For
	 * example, the first point is at (x[0], y[0], z[0]) with color colors[0].
	 * Much more efficient than drawing individual points.
	 */
	public static Shape points(double[] x, double[] y, double[] z, Color[] colors) {

		Point3f[] coords = constructPoint3f(x, y, z);

		GeometryArray geom = new PointArray(coords.length, PointArray.COORDINATES | PointArray.COLOR_4);
		geom.setCoordinates(0, coords);

		for (int i = 0; i < x.length; i++)
			geom.setColor(i, colors[i].getComponents(null));

		Shape3D shape = createShape3D(geom);

		return customShape(shape);
	}

	// *********************************************************************************************

	/**
	 * Draws a single line from (x1, y1, z1) to (x2, y2, z2).
	 */
	public static Shape line(double x1, double y1, double z1, double x2, double y2, double z2) {

		return lines(new double[] { x1, x2 }, new double[] { y1, y2 }, new double[] { z1, z2 });
	}

	/**
	 * Draws a set of connected lines. For example, the first line is from
	 * (x[0], y[0], z[0]) to (x[1], y[1], z[1]), and the second line is from
	 * (x[1], y[1], z[1]) to (x[2], y[2], z[2]). Much more efficient than
	 * drawing individual lines.
	 */
	public static Shape lines(double[] x, double[] y, double[] z) {

		Point3f[] coords = constructPoint3f(x, y, z);

		GeometryArray geom = new LineStripArray(coords.length, LineArray.COORDINATES, new int[] { coords.length });
		geom.setCoordinates(0, coords);

		Shape3D shape = createShape3D(geom);

		return shape(shape);
	}

	/**
	 * Draws a set of connected lines. For example, the first line is from
	 * (x[0], y[0], z[0]) to (x[1], y[1], z[1]), and the second line is from
	 * (x[1], y[1], z[1]) to (x[2], y[2], z[2]). Much more efficient than
	 * drawing individual lines. Vertex colors are specified by the given array,
	 * and line colors are blends of its two vertex colors.
	 */
	public static Shape lines(double[] x, double[] y, double[] z, Color[] colors) {

		Point3f[] coords = constructPoint3f(x, y, z);

		GeometryArray geom = new LineStripArray(coords.length, LineArray.COORDINATES | LineArray.COLOR_4,
				new int[] { coords.length });
		geom.setCoordinates(0, coords);

		for (int i = 0; i < x.length; i++)
			geom.setColor(i, colors[i].getComponents(null));

		Shape3D shape = createShape3D(geom);

		return customShape(shape);
	}

	/**
	 * Draws a cylindrical tube of radius r from vertex (x1, y1, z1) to vertex
	 * (x2, y2, z2).
	 */
	public static Shape tube(double x1, double y1, double z1, double x2, double y2, double z2, double r) {

		Vector3D mid = new Vector3D(x1 + x2, y1 + y2, z1 + z2).times(0.5);
		Vector3D line = new Vector3D(x2 - x1, y2 - y1, z2 - z1);

		Shape s = cylinder(mid.x, mid.y, mid.z, r, line.mag());

		Vector3D yAxis = new Vector3D(0, 1, 0);
		Vector3D cross = line.cross(yAxis);
		double angle = line.angle(yAxis);
		s.rotateAxis(cross, -angle);

		return combine(s);
	}

	/**
	 * Draws a series of cylindrical tubes of radius r, with vertices (x, y, z).
	 */
	public static Shape tubes(double[] x, double[] y, double[] z, double r) {

		StdDraw3D.Shape[] shapes = new StdDraw3D.Shape[(x.length - 1) * 2];

		for (int i = 0; i < x.length - 1; i++) {
			shapes[i] = tube(x[i], y[i], z[i], x[i + 1], y[i + 1], z[i + 1], r);
			shapes[i + x.length - 1] = sphere(x[i + 1], y[i + 1], z[i + 1], r);
		}

		return combine(shapes);
	}

	/**
	 * Draws a series of cylindrical tubes of radius r, with vertices (x, y, z)
	 * and the given colors. No more efficient than drawing individual tubes.
	 */
	public static Shape tubes(double[] x, double[] y, double[] z, double r, Color[] colors) {

		StdDraw3D.Shape[] shapes = new StdDraw3D.Shape[(x.length - 1) * 2];

		for (int i = 0; i < x.length - 1; i++) {
			StdDraw3D.setPenColor(colors[i]);
			shapes[i] = tube(x[i], y[i], z[i], x[i + 1], y[i + 1], z[i + 1], r);
			shapes[i + x.length - 1] = sphere(x[i + 1], y[i + 1], z[i + 1], r);
		}

		return combine(shapes);
	}

	// *********************************************************************************************

	/**
	 * Draws a filled polygon with the given vertices. The vertices should be
	 * planar for a proper 2D polygon to be drawn.
	 */
	public static Shape polygon(double[] x, double[] y, double[] z) {
		return polygon(x, y, z, true);
	}

	/**
	 * Draws the triangulated outline of a polygon with the given vertices.
	 */
	public static Shape wirePolygon(double[] x, double[] y, double[] z) {
		return polygon(x, y, z, false);
	}

	/**
	 * Draws a polygon with the given vertices, which is filled or outlined
	 * based on the argument.
	 * 
	 * @param filled
	 *            Is the polygon filled?
	 */
	private static Shape polygon(double[] x, double[] y, double[] z, boolean filled) {

		Point3f[] coords = constructPoint3f(x, y, z);
		GeometryArray geom = new TriangleFanArray(coords.length, LineArray.COORDINATES, new int[] { coords.length });
		geom.setCoordinates(0, coords);

		GeometryInfo geoinfo = new GeometryInfo((GeometryArray) geom);
		NormalGenerator normalGenerator = new NormalGenerator();
		normalGenerator.generateNormals(geoinfo);

		Shape3D shape = createShape3D(geoinfo.getIndexedGeometryArray());

		if (filled)
			return shape(shape);
		else
			return wireShape(shape);
	}

	// *********************************************************************************************

	/**
	 * Draws triangles which are defined by x1=points[i][0], y1=points[i][1],
	 * z1=points[i][2], x2=points[i][3], etc. All of the points will be the
	 * width and color specified by the setPenColor and setPenWidth methods.
	 * 
	 * @param points
	 *            an array of the points to be connected. The first dimension is
	 *            unspecified, but the second dimension should be 9 (the 3-space
	 *            coordinates of each vertex)
	 */
	public static Shape triangles(double[][] points) {
		return triangles(points, true);
	}

	public static Shape wireTriangles(double[][] points) {
		return triangles(points, false);
	}

	private static Shape triangles(double[][] points, boolean filled) {

		int size = points.length;
		Point3f[] coords = new Point3f[size * 3];

		for (int i = 0; i < size; i++) {
			coords[3 * i] = new Point3f(createVector3f(points[i][0], points[i][1], points[i][2]));
			coords[3 * i + 1] = new Point3f(createVector3f(points[i][3], points[i][4], points[i][5]));
			coords[3 * i + 2] = new Point3f(createVector3f(points[i][6], points[i][7], points[i][8]));
		}

		GeometryArray geom = new TriangleArray(size * 3, TriangleArray.COORDINATES);
		geom.setCoordinates(0, coords);

		GeometryInfo geoinfo = new GeometryInfo(geom);
		NormalGenerator normalGenerator = new NormalGenerator();
		normalGenerator.generateNormals(geoinfo);

		Shape3D shape = createShape3D(geoinfo.getIndexedGeometryArray());

		if (filled)
			return shape(shape);
		else
			return wireShape(shape);
	}

	/**
	 * Draws a set of triangles, each with the specified color. There should be
	 * one color for each triangle. This is much more efficient than drawing
	 * individual triangles.
	 */
	public static Shape triangles(double[][] points, Color[] colors) {
		return triangles(points, colors, true);
	}

	/**
	 * Draws a set of wireframetriangles, each with the specified color. There
	 * should be one color for each triangle. This is much more efficient than
	 * drawing individual triangles.
	 */
	public static Shape wireTriangles(double[][] points, Color[] colors) {
		return triangles(points, colors, false);
	}

	private static Shape triangles(double[][] points, Color[] colors, boolean filled) {
		int size = points.length;
		Point3f[] coords = new Point3f[size * 3];

		for (int i = 0; i < size; i++) {
			coords[3 * i] = new Point3f(createVector3f(points[i][0], points[i][1], points[i][2]));
			coords[3 * i + 1] = new Point3f(createVector3f(points[i][3], points[i][4], points[i][5]));
			coords[3 * i + 2] = new Point3f(createVector3f(points[i][6], points[i][7], points[i][8]));
		}

		GeometryArray geom = new TriangleArray(size * 3, TriangleArray.COORDINATES | TriangleArray.COLOR_4);
		geom.setCoordinates(0, coords);

		for (int i = 0; i < colors.length; i++) {
			geom.setColor(3 * i + 0, colors[i].getComponents(null));
			geom.setColor(3 * i + 1, colors[i].getComponents(null));
			geom.setColor(3 * i + 2, colors[i].getComponents(null));
		}

		GeometryInfo geoinfo = new GeometryInfo(geom);
		NormalGenerator normalGenerator = new NormalGenerator();
		normalGenerator.generateNormals(geoinfo);

		Shape3D shape = createShape3D(geoinfo.getIndexedGeometryArray());

		if (filled)
			return shape(shape);
		else
			return wireShape(shape);
	}

	// *********************************************************************************************

	/**
	 * Draws a 3D text object at (x, y, z). Uses the pen font, color, and
	 * transparency.
	 */
	public static Shape text3D(double x, double y, double z, String text) {

		return text3D(x, y, z, text, 0, 0, 0);
	}

	/**
	 * Draws a 3D text object at (x, y, z) with Euler rotation angles (xA, yA,
	 * zA). Uses the pen font, color, and transparency.
	 */
	public static Shape text3D(double x, double y, double z, String text, double xA, double yA, double zA) {

		Line2D.Double line = new Line2D.Double(0, 0, TEXT3D_DEPTH, 0);
		FontExtrusion extrudePath = new FontExtrusion(line);
		Font3D font3D = new Font3D(font, extrudePath);
		javax.media.j3d.Text3D t = new javax.media.j3d.Text3D(font3D, text, createPoint3f(x, y, z));

		// FIX THIS TO NOT HAVE SCALE INCLUDED
		Transform3D shrinker = new Transform3D();
		shrinker.setEuler(new Vector3d(xA, yA, zA));
		shrinker.setTranslation(new Vector3d(x, y, z));
		shrinker.setScale(TEXT3D_SHRINK_FACTOR);
		Shape3D shape = createShape3D((Geometry) t);
		return shape(shape, true, shrinker, false);
	}

	// *************************************************************************
	/**
	 * Constructs a Point3f array from the given coordinate arrays.
	 */
	private static Point3f[] constructPoint3f(double[] x, double[] y, double[] z) {

		int size = x.length;
		Point3f[] coords = new Point3f[size];

		for (int i = 0; i < size; i++)
			coords[i] = new Point3f(createVector3f(x[i], y[i], z[i]));

		return coords;
	}

	/**
	 * Draws a .ply file from a filename or website name.
	 */
	private static Shape drawPLY(String filename, boolean colored) {

		Scanner scanner = createScanner(filename);

		int vertices = -1;
		int triangles = -1;
		int properties = -1;

		while (true) {
			String s = scanner.next();
			if (s.equals("vertex"))
				vertices = scanner.nextInt();
			else if (s.equals("face"))
				triangles = scanner.nextInt();
			else if (s.equals("property")) {
				properties++;
				scanner.next();
				scanner.next();
			} else if (s.equals("end_header"))
				break;
		}

		System.out.println(vertices + " " + triangles + " " + properties);

		if ((vertices == -1) || (triangles == -1) || (properties == -1))
			throw new RuntimeException("Cannot read format of .ply file!");

		double[][] parameters = new double[properties][vertices];

		for (int i = 0; i < vertices; i++) {
			if ((i % 10000) == 0)
				System.out.println("vertex " + i);
			for (int j = 0; j < properties; j++) {
				parameters[j][i] = scanner.nextDouble();
			}
		}

		double[][] points = new double[triangles][9];

		for (int i = 0; i < triangles; i++) {
			int edges = scanner.nextInt();
			if (edges != 3)
				throw new RuntimeException("Only triangular faces supported!");

			if ((i % 10000) == 0)
				System.out.println("face " + i);

			int index = scanner.nextInt();
			points[i][0] = parameters[0][index];
			points[i][1] = parameters[1][index];
			points[i][2] = parameters[2][index];

			index = scanner.nextInt();
			points[i][3] = parameters[0][index];
			points[i][4] = parameters[1][index];
			points[i][5] = parameters[2][index];

			index = scanner.nextInt();
			points[i][6] = parameters[0][index];
			points[i][7] = parameters[1][index];
			points[i][8] = parameters[2][index];
		}

		return triangles(points);
	}

	// private static Shape drawLWS (String filename) {
	//
	// Lw3dLoader loader = new Lw3dLoader();
	// try {
	// BranchGroup bg = loader.load(filename).getSceneGroup();
	// bg.setCapability(BranchGroup.ALLOW_CHILDREN_READ);
	// bg.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
	// bg.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
	// bg.setCapability(BranchGroup.ALLOW_DETACH);
	//
	// TransformGroup transGroup = new TransformGroup();
	// transGroup.addChild(bg);
	// BranchGroup bg2 = createBranchGroup();
	// bg2.addChild(transGroup);
	// offscreenGroup.addChild(bg2);
	// return new Shape(bg2, transGroup);
	// } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); }
	// return null;
	// }

	private static Shape drawOBJ(String filename, boolean colored, boolean resize) {

		int params = 0;
		if (resize)
			params = ObjectFile.RESIZE | Loader.LOAD_ALL;

		ObjectFile loader = new ObjectFile(params);
		try {
			BranchGroup bg = loader.load(filename).getSceneGroup();
			bg.setCapability(BranchGroup.ALLOW_CHILDREN_READ);
			bg.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
			bg.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
			bg.setCapability(BranchGroup.ALLOW_DETACH);

			// System.out.println("Children: " + bg.numChildren());

			for (int i = 0; i < bg.numChildren(); i++) {
				Node child = bg.getChild(i);
				if (child instanceof Shape3D) {
					Shape3D shape = (Shape3D) child;
					// System.out.println("shape3d");
					// Appearance ap = shape.getAppearance();
					// PolygonAttributes pa = ap.getPolygonAttributes();
					// if (pa == null) pa = new PolygonAttributes();
					// pa.setCullFace(PolygonAttributes.CULL_NONE);
					// ap.setPolygonAttributes(pa);
					// Material m = ap.getMaterial();
					// m.setSpecularColor(new Color3f(GRAY));
					// m.setShininess(64);
					if (colored)
						shape.setAppearance(createAppearance(null, true));
					else {
						Appearance ap = shape.getAppearance();
						PolygonAttributes pa = ap.getPolygonAttributes();
						if (pa == null)
							pa = new PolygonAttributes();
						pa.setCullFace(PolygonAttributes.CULL_NONE);
						ap.setPolygonAttributes(pa);
					}
					// for (int j = 0; j < shape.numGeometries(); j++) {
					// Geometry g = shape.getGeometry(j);
					// if (g instanceof GeometryArray) {
					// GeometryArray ga = (GeometryArray) g;
					// System.out.println("GeometryArray");
					// System.out.println("format: " + ga.getVertexFormat());
					// float[] colors = ga.getInterleavedVertices();
					// for (int k = 0; k < colors.length; k++)
					// System.out.println(colors[k]);
					// }
					// }
				}
			}

			TransformGroup transGroup = new TransformGroup();
			transGroup.addChild(bg);
			BranchGroup bg2 = createBranchGroup();
			bg2.addChild(transGroup);
			offscreenGroup.addChild(bg2);
			return new Shape(bg2, transGroup);
		} catch (FileNotFoundException fnfe) {
			fnfe.printStackTrace();
		}
		return null;
	}

	public static Shape model(String filename) {
		return model(filename, false);
	}

	public static Shape model(String filename, boolean resize) {
		return model(filename, false, resize);
	}

	public static Shape coloredModel(String filename) {
		return model(filename, true, true);
	}

	public static Shape coloredModel(String filename, boolean resize) {
		return model(filename, true, resize);
	}

	private static Shape model(String filename, boolean colored, boolean resize) {

		if (filename == null)
			return null;
		String suffix = filename.substring(filename.lastIndexOf('.') + 1);

		if (suffix.equals("ply"))
			return drawPLY(filename, colored);
		else if (suffix.equals("obj"))
			return drawOBJ(filename, colored, resize);
		// else if (suffix.equals("lws"))
		// return drawLWS(filename);
		else
			throw new RuntimeException("Format not supported!");
	}

	/**
	 * Draws a Java 3D Shape3D object and fills it in.
	 * 
	 * @param shape
	 *            The Shape3D object to be drawn.
	 */
	private static Shape shape(Shape3D shape) {
		return shape(shape, true, null, false);
	}

	/**
	 * Draws a wireframe Java 3D Shape3D object and fills it in.
	 * 
	 * @param shape
	 *            The Shape3D object to be drawn.
	 */
	private static Shape wireShape(Shape3D shape) {
		return shape(shape, false, null, false);
	}

	private static Shape customShape(Shape3D shape) {
		return shape(shape, true, null, true);
	}

	// private static Shape customWireShape (Shape3D shape) {
	// return shape(shape, false, null, true);
	// }

	/**
	 * Draws a Java3D Shape3D object with the given properties.
	 * 
	 * @param polygonFill
	 *            Polygon fill properties, specified by Java 3D.
	 */
	private static Shape shape(Shape3D shape, boolean fill, Transform3D transform, boolean custom) {

		Appearance ap;

		if (custom)
			ap = createCustomAppearance(fill);
		else
			ap = createAppearance(null, fill);

		shape.setAppearance(ap);

		TransformGroup transGroup = new TransformGroup();
		if (transform != null)
			transGroup.setTransform(transform);

		transGroup.addChild(shape);

		BranchGroup bg = createBranchGroup();
		bg.addChild(transGroup);
		offscreenGroup.addChild(bg);
		return new Shape(bg, transGroup);
	}

	/* ***************************************************************
	 * 2D Overlay Drawing Methods *
	 * ***************************************************************
	 */

	/**
	 * Draws one pixel at (x, y).
	 */
	public static void overlayPixel(double x, double y) {
		getGraphics2D(offscreenImage).fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1);
	}

	/**
	 * Draws a point at (x, y).
	 */
	public static void overlayPoint(double x, double y) {
		float r = penRadius;
		if (r <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).fill(new Ellipse2D.Double(scaleX(x) - r / 2, scaleY(y) - r / 2, r, r));
	}

	/**
	 * Draws a line from (x0, y0) to (x1, y1).
	 */
	public static void overlayLine(double x0, double y0, double x1, double y1) {
		getGraphics2D(offscreenImage).draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1)));
	}

	/**
	 * Draws a circle of radius r, centered on (x, y).
	 */
	public static void overlayCircle(double x, double y, double r) {

		if (r < 0)
			throw new RuntimeException("circle radius can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * r);
		double hs = factorY(2 * r);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).draw(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws a filled circle of radius r, centered on (x, y).
	 */
	public static void overlayFilledCircle(double x, double y, double r) {

		if (r < 0)
			throw new RuntimeException("circle radius can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * r);
		double hs = factorY(2 * r);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).fill(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws an ellipse with given semimajor and semiminor axes, centered on (x,
	 * y).
	 */
	public static void overlayEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
		if (semiMajorAxis < 0)
			throw new RuntimeException("ellipse semimajor axis can't be negative");
		if (semiMinorAxis < 0)
			throw new RuntimeException("ellipse semiminor axis can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * semiMajorAxis);
		double hs = factorY(2 * semiMinorAxis);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).draw(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws a filled ellipse with given semimajor and semiminor axes, centered
	 * on (x, y).
	 */
	public static void overlayFilledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
		if (semiMajorAxis < 0)
			throw new RuntimeException("ellipse semimajor axis can't be negative");
		if (semiMinorAxis < 0)
			throw new RuntimeException("ellipse semiminor axis can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * semiMajorAxis);
		double hs = factorY(2 * semiMinorAxis);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).fill(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws an arc of radius r, centered on (x, y), from angle1 to angle2 (in
	 * degrees).
	 */
	public static void overlayArc(double x, double y, double r, double angle1, double angle2) {
		if (r < 0)
			throw new RuntimeException("arc radius can't be negative");
		while (angle2 < angle1)
			angle2 += 360;
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * r);
		double hs = factorY(2 * r);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).draw(
					new Arc2D.Double(xs - ws / 2, ys - hs / 2, ws, hs, angle1, angle2 - angle1, Arc2D.OPEN));
	}

	/**
	 * Draws a square of side length 2r, centered on (x, y).
	 */
	public static void overlaySquare(double x, double y, double r) {
		if (r < 0)
			throw new RuntimeException("square side length can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * r);
		double hs = factorY(2 * r);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).draw(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws a filled square of side length 2r, centered on (x, y).
	 */
	public static void overlayFilledSquare(double x, double y, double r) {
		if (r < 0)
			throw new RuntimeException("square side length can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * r);
		double hs = factorY(2 * r);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).fill(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws a rectangle of given half width and half height, centered on (x,
	 * y).
	 */
	public static void overlayRectangle(double x, double y, double halfWidth, double halfHeight) {
		if (halfWidth < 0)
			throw new RuntimeException("half width can't be negative");
		if (halfHeight < 0)
			throw new RuntimeException("half height can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * halfWidth);
		double hs = factorY(2 * halfHeight);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).draw(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws a filled rectangle of given half width and half height, centered on
	 * (x, y).
	 */
	public static void overlayFilledRectangle(double x, double y, double halfWidth, double halfHeight) {
		if (halfWidth < 0)
			throw new RuntimeException("half width can't be negative");
		if (halfHeight < 0)
			throw new RuntimeException("half height can't be negative");
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(2 * halfWidth);
		double hs = factorY(2 * halfHeight);
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else
			getGraphics2D(offscreenImage).fill(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs));
	}

	/**
	 * Draws a polygon with the given (x[i], y[i]) coordinates.
	 */
	public static void overlayPolygon(double[] x, double[] y) {
		int N = x.length;
		GeneralPath path = new GeneralPath();
		path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
		for (int i = 0; i < N; i++)
			path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
		path.closePath();
		getGraphics2D(offscreenImage).draw(path);
	}

	/**
	 * Draws a filled polygon with the given (x[i], y[i]) coordinates.
	 */
	public static void overlayFilledPolygon(double[] x, double[] y) {
		int N = x.length;
		GeneralPath path = new GeneralPath();
		path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
		for (int i = 0; i < N; i++)
			path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
		path.closePath();
		getGraphics2D(offscreenImage).fill(path);
	}

	/**
	 * Draws the given text as stationary on the window at (x, y). This is
	 * useful for titles and HUD-style text.
	 */
	public static void overlayText(double x, double y, String text) {
		Graphics2D graphics = getGraphics2D(offscreenImage);
		FontMetrics metrics = graphics.getFontMetrics();
		double xs = scaleX(x);
		double ys = scaleY(y);
		int ws = metrics.stringWidth(text);
		int hs = metrics.getDescent();
		graphics.drawString(text, (float) (xs - ws / 2.0), (float) (ys + hs));
	}

	/**
	 * Writes the given text string in the current font, centered on (x, y) and
	 * rotated by the specified number of degrees.
	 */
	public static void overlayText(double x, double y, String text, double degrees) {
		Graphics2D graphics = getGraphics2D(offscreenImage);
		FontMetrics metrics = graphics.getFontMetrics();
		double xs = scaleX(x);
		double ys = scaleY(y);
		int ws = metrics.stringWidth(text);
		int hs = metrics.getDescent();
		graphics.rotate(Math.toRadians(-degrees), xs, ys);
		graphics.drawString(text, (float) (xs - ws / 2.0), (float) (ys + hs));
		graphics.rotate(Math.toRadians(+degrees), xs, ys);
	}

	/**
	 * Write the given text string in the current font, left-aligned at (x, y).
	 */
	public static void overlayTextLeft(double x, double y, String text) {
		Graphics2D graphics = getGraphics2D(offscreenImage);
		FontMetrics metrics = graphics.getFontMetrics();
		double xs = scaleX(x);
		double ys = scaleY(y);
		int hs = metrics.getDescent();
		graphics.drawString(text, (float) (xs), (float) (ys + hs));
	}

	/**
	 * Write the given text string in the current font, right-aligned at (x, y).
	 */
	public static void overlayTextRight(double x, double y, String text) {
		Graphics2D graphics = getGraphics2D(offscreenImage);
		FontMetrics metrics = graphics.getFontMetrics();
		double xs = scaleX(x);
		double ys = scaleY(y);
		int ws = metrics.stringWidth(text);
		int hs = metrics.getDescent();
		graphics.drawString(text, (float) (xs - ws), (float) (ys + hs));
	}

	/**
	 * Draws a picture (gif, jpg, or png) centered on (x, y).
	 */
	public static void overlayPicture(double x, double y, String s) {
		Image image = getImage(s);
		double xs = scaleX(x);
		double ys = scaleY(y);
		int ws = image.getWidth(null);
		int hs = image.getHeight(null);
		if (ws < 0 || hs < 0)
			throw new RuntimeException("image " + s + " is corrupt");
		getGraphics2D(offscreenImage).drawImage(image, (int) Math.round(xs - ws / 2.0),
				(int) Math.round(ys - hs / 2.0), null);
	}

	/**
	 * Draws a picture (gif, jpg, or png) centered on (x, y), rotated given
	 * number of degrees.
	 */
	public static void overlayPicture(double x, double y, String s, double degrees) {
		Image image = getImage(s);
		double xs = scaleX(x);
		double ys = scaleY(y);
		int ws = image.getWidth(null);
		int hs = image.getHeight(null);
		if (ws < 0 || hs < 0)
			throw new RuntimeException("image " + s + " is corrupt");

		Graphics2D graphics = getGraphics2D(offscreenImage);
		graphics.rotate(Math.toRadians(-degrees), xs, ys);
		graphics.drawImage(image, (int) Math.round(xs - ws / 2.0), (int) Math.round(ys - hs / 2.0), null);
		graphics.rotate(Math.toRadians(+degrees), xs, ys);
	}

	/**
	 * Draw picture (gif, jpg, or png) centered on (x, y), rescaled to w-by-h.
	 */
	public static void overlayPicture(double x, double y, String s, double w, double h) {
		Image image = getImage(s);
		double xs = scaleX(x);
		double ys = scaleY(y);
		if (w < 0)
			throw new RuntimeException("width is negative: " + w);
		if (h < 0)
			throw new RuntimeException("height is negative: " + h);
		double ws = factorX(w);
		double hs = factorY(h);
		if (ws < 0 || hs < 0)
			throw new RuntimeException("image " + s + " is corrupt");
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);
		else {
			getGraphics2D(offscreenImage).drawImage(image, (int) Math.round(xs - ws / 2.0),
					(int) Math.round(ys - hs / 2.0), (int) Math.round(ws), (int) Math.round(hs), null);
		}
	}

	/**
	 * Draw picture (gif, jpg, or png) centered on (x, y), rotated given number
	 * of degrees, rescaled to w-by-h.
	 */
	public static void overlayPicture(double x, double y, String s, double w, double h, double degrees) {
		Image image = getImage(s);
		double xs = scaleX(x);
		double ys = scaleY(y);
		double ws = factorX(w);
		double hs = factorY(h);
		if (ws < 0 || hs < 0)
			throw new RuntimeException("image " + s + " is corrupt");
		if (ws <= 1 && hs <= 1)
			overlayPixel(x, y);

		Graphics2D graphics = getGraphics2D(offscreenImage);
		graphics.rotate(Math.toRadians(-degrees), xs, ys);
		graphics.drawImage(image, (int) Math.round(xs - ws / 2.0), (int) Math.round(ys - hs / 2.0),
				(int) Math.round(ws), (int) Math.round(hs), null);
		graphics.rotate(Math.toRadians(+degrees), xs, ys);
	}

	/**
	 * Gets an image from the given filename.
	 */
	private static Image getImage(String filename) {

		// to read from file
		ImageIcon icon = new ImageIcon(filename);

		// try to read from URL
		if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
			try {
				URL url = new URL(filename);
				icon = new ImageIcon(url);
			} catch (Exception e) { /* not a url */
			}
		}

		// in case file is inside a .jar
		if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
			URL url = StdDraw3D.class.getResource(filename);
			if (url == null)
				throw new RuntimeException("image " + filename + " not found");
			icon = new ImageIcon(url);
		}

		return icon.getImage();
	}

	private static Graphics2D getGraphics2D(BufferedImage image) {

		Graphics2D graphics = (Graphics2D) image.getGraphics();
		graphics.setColor(penColor);
		graphics.setFont(font);
		BasicStroke stroke = new BasicStroke(penRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
		graphics.setStroke(stroke);

		// if (getAntiAliasing())
		graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		return graphics;
	}

	private static void infoDisplay() {

		if (!infoDisplay) {
			infoImage = createBufferedImage();
			return;
		}

		BufferedImage bi = createBufferedImage();
		Graphics2D g = (Graphics2D) bi.getGraphics();
		g.setFont(new Font("Courier", Font.PLAIN, 11));
		g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));

		double center = (min + max) / 2;
		double b = zoom * 0.1f;

		DecimalFormat df = new DecimalFormat(" 0.000;-0.000");

		Vector3D pos = camera.getPosition();
		String s = "(" + df.format(pos.x) + "," + df.format(pos.y) + "," + df.format(pos.z) + ")";
		g.setColor(BLACK);
		g.drawString("Position: " + s, 21, 26);
		g.setColor(LIGHT_GRAY);
		g.drawString("Position: " + s, 20, 25);

		Vector3D rot = camera.getOrientation();
		String s2 = "(" + df.format(rot.x) + "," + df.format(rot.y) + "," + df.format(rot.z) + ")";
		g.setColor(BLACK);
		g.drawString("Rotation: " + s2, 21, 41);
		g.setColor(LIGHT_GRAY);
		g.drawString("Rotation: " + s2, 20, 40);

		String mode;
		if (cameraMode == ORBIT_MODE)
			mode = "Camera: ORBIT_MODE";
		else if (cameraMode == FPS_MODE)
			mode = "Camera: FPS_MODE";
		else if (cameraMode == AIRPLANE_MODE)
			mode = "Camera: AIRPLANE_MODE";
		else if (cameraMode == LOOK_MODE)
			mode = "Camera: LOOK_MODE";
		else if (cameraMode == FIXED_MODE)
			mode = "Camera: FIXED_MODE";
		else
			throw new RuntimeException("Unknown camera mode!");

		g.setColor(BLACK);
		g.drawString(mode, 21, 56);
		g.setColor(LIGHT_GRAY);
		g.drawString(mode, 20, 55);

		double d = b / 4;
		g.draw(new Line2D.Double(scaleX(d + center), scaleY(0 + center), scaleX(-d + center), scaleY(0 + center)));
		g.draw(new Line2D.Double(scaleX(0 + center), scaleY(d + center), scaleX(0 + center), scaleY(-d + center)));

		infoImage = bi;
	}

	/* ***************************************************************
	 * Saving/Loading Methods *
	 * ***************************************************************
	 */

	/**
	 * Saves to file - suffix must be png, jpg, or gif. gif??
	 * 
	 * @param filename
	 *            The name of the file with one of the required suffixes.
	 */
	public static void save(String filename) {

		// canvas.setVisible(false);
		int oldCameraMode = getCameraMode();
		setCameraMode(FIXED_MODE);

		GraphicsContext3D context = canvas.getGraphicsContext3D();

		BufferedImage buf = createBufferedImage();
		ImageComponent2D imageComp = new ImageComponent2D(ImageComponent.FORMAT_RGB, buf);
		javax.media.j3d.Raster ras = new javax.media.j3d.Raster(new Point3f(-1.0f, -1.0f, -1.0f),
				javax.media.j3d.Raster.RASTER_COLOR, 0, 0, width, height, imageComp, null);

		context.readRaster(ras);

		BufferedImage image = (ras.getImage()).getImage();

		File file = new File(filename);
		String suffix = filename.substring(filename.lastIndexOf('.') + 1);

		String extension = suffix.toLowerCase();

		// png files
		if (extension.equals("png")) {
			try {
				ImageIO.write(image, suffix, file);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		// need to change from ARGB to RGB for jpeg
		// reference:
		// http://archives.java.sun.com/cgi-bin/wa?A2=ind0404&L=java2d-interest&D=0&P=2727
		else if (extension.equals("jpg")) {
			WritableRaster raster = image.getRaster();
			WritableRaster newRaster;
			newRaster = raster.createWritableChild(0, 0, width, height, 0, 0, new int[] { 0, 1, 2 });
			BufferedImage rgbBuffer = new BufferedImage(image.getColorModel(), newRaster, false, null);
			try {
				ImageIO.write(rgbBuffer, suffix, file);
			} catch (IOException e) {
				e.printStackTrace();
			}
		} else {
			System.out.println("Invalid image file type: " + suffix);
		}

		setCameraMode(oldCameraMode);
		// canvas.setVisible(true);
	}

	public static void saveScene3D(String filename) {

		File file = new File(filename);
		// System.out.println("on: " + onscreenGroup.numChildren() + " off: " +
		// offscreenGroup.numChildren());

		try {
			SceneGraphFileWriter writer = new SceneGraphFileWriter(file, universe, false,
					"3D scene saved from StdDraw3D.", null);
			writer.writeBranchGraph(offscreenGroup);
			writer.close();
			System.out.println("Scene successfully written to " + filename + "!");
		} catch (IOException ioe) {
			ioe.printStackTrace();
		} catch (UnsupportedUniverseException uue) {
			uue.printStackTrace();
		}
		// catch (NamedObjectException noe) { noe.printStackTrace(); }
	}

	public static void loadScene3D(String filename) {

		File file = new File(filename);

		try {
			SceneGraphFileReader reader = new SceneGraphFileReader(file);
			System.out.println("Branch graph count = " + reader.getBranchGraphCount());
			// BranchGroup[] bgs = reader.readAllBranchGraphs();
			// BranchGroup bg = (BranchGroup) reader.getNamedObject("rendered");
			BranchGroup bg = reader.readBranchGraph(0)[0];
			offscreenGroup = bg;
			// reader.dereferenceBranchGraph(offscreenGroup);
			System.out.println("Scene successfully loaded from " + filename + "!");
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
		// catch (ObjectNotLoadedException onle) { onle.printStackTrace(); }
		// catch (NamedObjectException noe) { noe.printStackTrace(); }
	}

	/* ***************************************************************
	 * Shape Methods *
	 * ***************************************************************
	 */

	/**
	 * Combines any number of shapes into one shape and returns it.
	 */
	public static Shape combine(Shape... shapes) {

		BranchGroup combinedGroup = createBranchGroup();
		TransformGroup combinedTransform = new TransformGroup();

		for (int i = 0; i < shapes.length; i++) {
			BranchGroup bg = shapes[i].bg;
			TransformGroup tg = shapes[i].tg;

			offscreenGroup.removeChild(bg);
			onscreenGroup.removeChild(bg);

			bg.removeChild(tg);
			combinedTransform.addChild(shapes[i].tg);
		}

		combinedGroup.addChild(combinedTransform);
		offscreenGroup.addChild(combinedGroup);
		return new Shape(combinedGroup, combinedTransform);
	}

	/**
	 * Returns an identical copy of a Shape that can be controlled
	 * independently. Much more efficient than redrawing a specific shape or
	 * model.
	 */
	public static Shape copy(Shape shape) {
		TransformGroup tg = shape.tg;
		TransformGroup tg2 = (TransformGroup) tg.cloneTree();
		BranchGroup bg2 = createBranchGroup();

		bg2.addChild(tg2);
		offscreenGroup.addChild(bg2);

		return new Shape(bg2, tg2);
	}

	/**
	 * Private class that represents anything that can be moved and rotated.
	 */
	private static class Transformable {

		private TransformGroup tg;

		private Transformable(TransformGroup tg0) {
			this.tg = tg0;
		}

		private Transform3D getTransform() {
			Transform3D t = new Transform3D();
			tg.getTransform(t);
			return t;
		}

		private void setTransform(Transform3D t) {
			tg.setTransform(t);
		}

		private Vector3D relToAbs(Vector3D r) {
			Transform3D t = getTransform();

			Matrix3d m = new Matrix3d();
			t.get(m);
			Vector3d zero = new Vector3d(0, 0, 0);
			Transform3D rotation = new Transform3D(m, zero, 1.0);
			Vector3f vec = createVector3f(r);
			rotation.transform(vec);

			return new Vector3D(vec);
		}

		private Vector3D absToRel(Vector3D r) {
			Transform3D t = getTransform();

			Matrix3d m = new Matrix3d();
			t.get(m);
			Vector3d zero = new Vector3d(0, 0, 0);
			Transform3D rotation = new Transform3D(m, zero, 1.0);
			Vector3f vec = createVector3f(r);
			rotation.invert();
			rotation.transform(vec);

			return new Vector3D(vec);
		}

		// private void rotateQuat (double x, double y, double z, double w) {
		// rotateQuat(new Quat4d(x, y, z, w));
		// }

		// private void rotateQuat (Quat4d quat) {
		// Transform3D t = getTransform();
		//
		// Transform3D t1 = new Transform3D();
		// t1.setRotation(quat);
		// t.mul(t1);
		//
		// setTransform(t);
		// }
		//
		// private void setQuaternion (double x, double y, double z, double w) {
		// setQuaternion(new Quat4d(x, y, z, w));
		// }

		// private void setQuaternion (Quat4d quat) {
		// Transform3D t = getTransform();
		//
		// t.setRotation(quat);
		//
		// setTransform(t);
		// }

		// private Quat4d getQuaternion () {
		// Transform3D t = getTransform();
		//
		// Matrix3d m = new Matrix3d();
		// t.get(m);
		//
		// double w = Math.sqrt(Math.max(0, 1 + m.m00 + m.m11 + m.m22))/2;
		// double x = Math.sqrt(Math.max(0, 1 + m.m00 - m.m11 - m.m22))/2;
		// double y = Math.sqrt(Math.max(0, 1 - m.m00 + m.m11 - m.m22))/2;
		// double z = Math.sqrt(Math.max(0, 1 - m.m00 - m.m11 + m.m22))/2;
		// if (m.m21 - m.m12 < 0) x = -x;
		// if (m.m02 - m.m20 < 0) y = -y;
		// if (m.m10 - m.m01 < 0) z = -z;
		//
		// return new Quat4d(x, y, z, w);
		// }

		// private void orientAxis (Vector3D axis, double angle) {
		// Transform3D t = getTransform();
		//
		// AxisAngle4d aa = new AxisAngle4d(axis.x, axis.y, axis.z,
		// Math.toRadians(angle));
		// t.setRotation(aa);
		//
		// setTransform(t);
		// }

		public void rotateAxis(Vector3D axis, double angle) {
			if (angle == 0)
				return;
			Transform3D t = getTransform();

			Vector3D aRel = absToRel(axis);
			AxisAngle4d aa = new AxisAngle4d(aRel.x, aRel.y, aRel.z, Math.toRadians(angle));
			Transform3D t1 = new Transform3D();
			t1.setRotation(aa);
			t.mul(t1);

			setTransform(t);
		}

		public void move(double x, double y, double z) {
			move(new Vector3D(x, y, z));
		}

		public void move(Vector3D move) {
			Transform3D t = getTransform();

			Vector3f r = new Vector3f();
			t.get(r);
			r.add(createVector3f(move));
			t.setTranslation(r);

			setTransform(t);
		}

		public void moveRelative(double right, double up, double forward) {
			moveRelative(new Vector3D(right, up, forward));
		}

		public void moveRelative(Vector3D move) {
			move(relToAbs(move.times(1, 1, -1)));
		}

		public void setPosition(double x, double y, double z) {
			setPosition(new Vector3D(x, y, z));
		}

		public void setPosition(Vector3D pos) {
			Transform3D t = getTransform();

			t.setTranslation(createVector3f(pos));

			setTransform(t);
		}

		public Vector3D getPosition() {
			Transform3D t = getTransform();
			Vector3d r = new Vector3d();
			t.get(r);
			return new Vector3D(r);
		}

		public void rotate(double xAngle, double yAngle, double zAngle) {
			rotate(new Vector3D(xAngle, yAngle, zAngle));
		}

		public void rotate(Vector3D angles) {
			Transform3D t = getTransform();

			Transform3D tX = new Transform3D();
			Transform3D tY = new Transform3D();
			Transform3D tZ = new Transform3D();

			Vector3D xR = absToRel(xAxis);
			Vector3D yR = absToRel(yAxis);
			Vector3D zR = absToRel(zAxis);

			Vector3D radians = angles.times(Math.PI / 180.);
			tX.setRotation(new AxisAngle4d(xR.x, xR.y, xR.z, radians.x));
			tY.setRotation(new AxisAngle4d(yR.x, yR.y, yR.z, radians.y));
			tZ.setRotation(new AxisAngle4d(zR.x, zR.y, zR.z, radians.z));

			t.mul(tX);
			t.mul(tY);
			t.mul(tZ);

			setTransform(t);
		}

		public void rotateRelative(double pitch, double yaw, double roll) {
			rotateRelative(new Vector3D(pitch, yaw, roll));
		}

		public void rotateRelative(Vector3D angles) {
			Transform3D t = getTransform();

			Transform3D tX = new Transform3D();
			Transform3D tY = new Transform3D();
			Transform3D tZ = new Transform3D();

			Vector3D radians = angles.times(Math.PI / 180.);
			tX.setRotation(new AxisAngle4d(1, 0, 0, radians.x));
			tY.setRotation(new AxisAngle4d(0, 1, 0, radians.y));
			tZ.setRotation(new AxisAngle4d(0, 0, 1, radians.z));

			t.mul(tX);
			t.mul(tY);
			t.mul(tZ);

			setTransform(t);
		}

		public void setOrientation(double xAngle, double yAngle, double zAngle) {
			setOrientation(new Vector3D(xAngle, yAngle, zAngle));
		}

		public void setOrientation(Vector3D angles) {
			if (Math.abs(angles.y) == 90)
				System.err.println("Gimbal lock when the y-angle is vertical!");

			Transform3D t = getTransform();

			Vector3D radians = angles.times(Math.PI / 180.);
			Transform3D t1 = new Transform3D();
			t1.setEuler(createVector3d(radians));
			Vector3d r = new Vector3d();
			t.get(r);
			t1.setTranslation(r);
			t1.setScale(t.getScale());

			setTransform(t1);
		}

		public Vector3D getOrientation() {
			Transform3D t = getTransform();

			Matrix3d mat = new Matrix3d();
			t.get(mat);

			double xA, yA, zA;

			yA = -Math.asin(mat.m20);
			double C = Math.cos(yA);
			if (Math.abs(C) > 0.005) {
				xA = -Math.atan2(-mat.m21 / C, mat.m22 / C);
				zA = -Math.atan2(-mat.m10 / C, mat.m00 / C);
			} else {
				xA = 0;
				zA = -Math.atan2(mat.m01, mat.m11);
			}

			xA = Math.toDegrees(xA);
			yA = Math.toDegrees(yA);
			zA = Math.toDegrees(zA);

			/* return only positive angles in [0,360] */
			if (xA < 0)
				xA += 360;
			if (yA < 0)
				yA += 360;
			if (zA < 0)
				zA += 360;

			return new Vector3D(xA, yA, zA);
		}

		public void lookAt(Vector3D center) {
			lookAt(center, yAxis);
		}

		public void lookAt(Vector3D center, Vector3D up) {
			Transform3D t = getTransform();

			Transform3D t2 = new Transform3D(t);

			Vector3f translation = new Vector3f();
			t2.get(translation);

			Vector3d scales = new Vector3d();
			t2.getScale(scales);

			Point3d trans = new Point3d(translation);
			Point3d c = new Point3d(center.x, center.y, center.z);
			Vector3d u = createVector3d(up);
			t2.lookAt(trans, c, u);

			try {
				t2.invert();
				t2.setScale(scales);
				setTransform(t2);
			} catch (SingularMatrixException sme) {
				System.out.println("Singular matrix, bad lookAt()!");
			}

		}

		public void setDirection(Vector3D direction) {
			setDirection(direction, yAxis);
		}

		public void setDirection(Vector3D direction, Vector3D up) {
			Vector3D center = getPosition().plus(direction);
			lookAt(center, up);
		}

		public Vector3D getDirection() {
			return relToAbs(zAxis.times(-1)).direction();
		}

		private void match(Transformable s) {
			setOrientation(s.getOrientation());
			setPosition(s.getPosition());
		}
	}

	public static Vector3D getCameraPosition() {
		return camera.getPosition();
	}

	public static Vector3D getCameraOrientation() {
		return camera.getOrientation();
	}

	public static Vector3D getCameraDirection() {
		return camera.getDirection();
	}

	public static void setCameraPosition(double x, double y, double z) {
		setCameraPosition(new Vector3D(x, y, z));
	}

	public static void setCameraPosition(Vector3D position) {
		camera.setPosition(position);
	}

	public static void setCameraOrientation(double xAngle, double yAngle, double zAngle) {
		setCameraOrientation(new Vector3D(xAngle, yAngle, zAngle));
	}

	public static void setCameraOrientation(Vector3D angles) {
		camera.setOrientation(angles);
	}

	public static void setCameraDirection(double x, double y, double z) {
		setCameraDirection(new Vector3D(x, y, z));
	}

	public static void setCameraDirection(Vector3D direction) {
		camera.setDirection(direction);
	}

	public static void setCamera(double x, double y, double z, double xAngle, double yAngle, double zAngle) {
		camera.setPosition(x, y, z);
		camera.setOrientation(xAngle, yAngle, zAngle);
	}

	public static void setCamera(Vector3D position, Vector3D angles) {
		camera.setPosition(position);
		camera.setOrientation(angles);
	}

	/**
	 * Returns the Camera object.
	 */
	public static Camera camera() {
		return camera;
	}

	/**
	 * The camera can be controlled with the static functions already listed in
	 * this reference. However, much more advanced control of the camera can be
	 * obtained by manipulating the Camera object. The Camera is basically
	 * equivalent to a Shape with a couple of extra functions, so it can be
	 * moved and rotated just like a Shape. This functionality works well with
	 * point-of-view simulations and games.
	 */
	public static class Camera extends Transformable {

		private TransformGroup tg;
		private Shape pair;

		private Camera(TransformGroup tg) {
			super(tg);
			this.tg = tg;
		}

		public void match(Shape s) {
			super.match(s);
		}

		public void pair(Shape s) {
			pair = s;
		}

		public void unpair() {
			pair = null;
		}

		public TransformGroup getTg() {
			return tg;
		}

		public void moveRelative(Vector3D move) {
			if ((view.getProjectionPolicy() == View.PARALLEL_PROJECTION)) {
				setScreenScale(view.getScreenScale() * (1 + move.z / zoom));
				super.move(super.relToAbs(move.times(1, 1, 0)));
			} else
				super.move(super.relToAbs(move.times(1, 1, -1)));
		}

		public void rotateFPS(Vector3D angles) {
			rotateFPS(angles.x, angles.y, angles.z);
		}

		public void rotateFPS(double xAngle, double yAngle, double zAngle) {

			double xA = Math.toRadians(xAngle);
			double yA = Math.toRadians(yAngle);
			double zA = Math.toRadians(zAngle);

			Vector3D shift = super.relToAbs(new Vector3D(-yA, xA, zA));
			Vector3D dir = super.getDirection().plus(shift);
			double angle = dir.angle(yAxis);
			if (angle > 90)
				angle = 180 - angle;
			if (angle < 5)
				return;
			super.setDirection(super.getDirection().plus(shift));
		}
	}

	/**
	 * Everything three-dimensional you draw in your scene from spheres to
	 * points to 3D text is actually a Shape object. When you call a drawing
	 * function, it always returns a Shape object. If you keep a reference to
	 * this object, you can manipulate the already drawn Shape instead of
	 * clearing and redrawing it, which is a much more powerful and more
	 * efficient method of animation.
	 */
	public static class Shape extends Transformable {

		private BranchGroup bg;
		private TransformGroup tg;

		private Shape(BranchGroup bg, TransformGroup tg) {
			super(tg);
			this.bg = bg;
			this.tg = tg;
			tg.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
			tg.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		}

		public void scale(double scale) {
			Transform3D t = super.getTransform();

			t.setScale(t.getScale() * scale);

			super.setTransform(t);
		}

		// public void scale (double xScale, double yScale, double zScale) {
		// scale(new Vector3D(xScale, yScale, zScale));
		// }
		//
		// public void scale (Vector3D scales) {
		// Transform3D t = getTransform();
		// throw new
		// UnsupportedOperationException("Doesn't work with rotation!");
		// setTransform(t);
		// }

		public void hide() {
			offscreenGroup.removeChild(bg);
			onscreenGroup.removeChild(bg);
		}

		public void unhide() {
			hide();
			offscreenGroup.addChild(bg);
		}

		public void match(Shape s) {
			super.match(s);
		}

		public void match(Camera c) {
			super.match(c);
		}

		public void setColor(Color c) {
			setColor(tg, c);
		}

		public void setColor(Color c, int alpha) {
			setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha));
		}

		private void setColor(Group g, Color c) {
			for (int i = 0; i < g.numChildren(); i++) {
				Node child = g.getChild(i);
				if (child instanceof Shape3D) {
					Shape3D shape = (Shape3D) child;
					Appearance ap = shape.getAppearance();
					setColor(ap, c);
				} else if (child instanceof Primitive) {
					Primitive primitive = (Primitive) child;
					Appearance ap = primitive.getAppearance();
					setColor(ap, c);
				} else if (child instanceof Group) {
					setColor((Group) child, c);
				}
			}
		}

		private void setColor(Appearance ap, Color c) {
			Material m = ap.getMaterial();
			m.setAmbientColor(new Color3f(c));
			m.setDiffuseColor(new Color3f(c));

			float alpha = ((float) c.getAlpha()) / 255;
			if (alpha < 1.0) {
				TransparencyAttributes t = new TransparencyAttributes();
				t.setTransparencyMode(TransparencyAttributes.BLENDED);
				t.setTransparency(1 - alpha);
				ap.setTransparencyAttributes(t);
			} else
				ap.setTransparencyAttributes(null);
		}
	}

	/**
	 * When you create a light in StdDraw3D, it returns a Light object. Light
	 * objects can be manipulated just like Shapes, and are useful if you want
	 * moving lights or lights that change color and brightness.
	 */
	public static class Light extends Transformable {

		javax.media.j3d.Light light;
		BranchGroup bg;

		private Light(BranchGroup bg, TransformGroup tg, javax.media.j3d.Light light) {
			super(tg);
			this.light = light;
			this.bg = bg;
		}

		public void hide() {
			light.setEnable(false);
		}

		public void unhide() {
			light.setEnable(true);
		}

		public void match(Shape s) {
			super.match(s);
		}

		public void match(Camera c) {
			super.match(c);
		}

		public void setColor(Color col) {
			light.setColor(new Color3f(col));
		}

		public void scalePower(double power) {

			if (light instanceof PointLight) {

				double attenuationScale = 1.0 / (0.999 * power + 0.001);

				PointLight pl = (PointLight) light;
				Point3f attenuation = new Point3f();
				pl.getAttenuation(attenuation);
				attenuation.y *= attenuationScale;
				attenuation.z *= attenuationScale * attenuationScale;

				pl.setAttenuation(attenuation);
			} else {
				System.err.println("Can only scale power for point lights!");
			}
		}
	}

	/******************************************************************************
	 * An immutable three-dimensional vector class with useful vector
	 * operations.
	 * 
	 * @author Hayk Martirosyan <hmartiro@princeton.edu>
	 * @since 2011.0310
	 * @version 1.0
	 ****************************************************************************/
	public static class Vector3D {

		/** X-coordinate - immutable but directly accessible. */
		public final double x;

		/** Y-coordinate - immutable but directly accessible. */
		public final double y;

		/** Z-coordinate - immutable but directly accessible. */
		public final double z;

		// --------------------------------------------------------------------------

		/**
		 * Initializes to zero vector.
		 */
		public Vector3D() {

			this.x = 0;
			this.y = 0;
			this.z = 0;
		}

		// --------------------------------------------------------------------------

		/**
		 * Initializes to the given coordinates.
		 * 
		 * @param x
		 *            X-coordinate
		 * @param y
		 *            Y-coordinate
		 * @param z
		 *            Z-coordinate
		 */
		public Vector3D(double x, double y, double z) {

			this.x = x;
			this.y = y;
			this.z = z;
		}

		/**
		 * Initializes to the given coordinates.
		 * 
		 * @param c
		 *            Array length 3 of coordinates (x, y, z,).
		 */
		// --------------------------------------------------------------------------

		public Vector3D(double[] c) {

			if (c.length != 3)
				throw new RuntimeException("Incorrect number of dimensions!");
			this.x = c[0];
			this.y = c[1];
			this.z = c[2];
		}

		// --------------------------------------------------------------------------

		private Vector3D(Vector3d v) {

			this.x = v.x;
			this.y = v.y;
			this.z = v.z;
		}

		// --------------------------------------------------------------------------

		private Vector3D(Vector3f v) {

			this.x = v.x;
			this.y = v.y;
			this.z = v.z;
		}

		// --------------------------------------------------------------------------

		private Vector3D(Point3d p) {

			this.x = p.x;
			this.y = p.y;
			this.z = p.z;

		}

		// --------------------------------------------------------------------------

		// private Vector3D (Point3f p) {
		//
		// this.x = p.x;
		// this.y = p.y;
		// this.z = p.z;
		//
		// }

		// --------------------------------------------------------------------------
		/*
		 * public Vector3D (double min, double max) {
		 * 
		 * this.x = min + Math.random() * (max - min); this.y = min +
		 * Math.random() * (max - min); this.z = min + Math.random() * (max -
		 * min); }
		 */
		// --------------------------------------------------------------------------
		/**
		 * Returns the dot product of this and that.
		 * 
		 * @param that
		 *            Vector to dot with
		 * @return This dot that
		 */
		public double dot(Vector3D that) {

			return (this.x * that.x + this.y * that.y + this.z * that.z);
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the magnitude of this vector.
		 * 
		 * @return Magnitude of vector
		 */
		public double mag() {

			return Math.sqrt(this.dot(this));
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the smallest angle between this and that vector, in DEGREES.
		 * 
		 * @return Angle between the vectors, in DEGREES
		 */
		public double angle(Vector3D that) {
			return Math.toDegrees(Math.acos(this.dot(that) / (this.mag() * that.mag())));
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the Euclidian distance between this and that.
		 * 
		 * @param that
		 *            Vector to compute distance between
		 * @return Distance between this and that
		 */
		public double distanceTo(Vector3D that) {
			return this.minus(that).mag();
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the sum of this and that vector.
		 * 
		 * @param that
		 *            Vector to compute sum with
		 * @return This plus that
		 */
		public Vector3D plus(Vector3D that) {

			double cx = this.x + that.x;
			double cy = this.y + that.y;
			double cz = this.z + that.z;
			Vector3D c = new Vector3D(cx, cy, cz);
			return c;
		}

		public Vector3D plus(double x, double y, double z) {
			double cx = this.x + x;
			double cy = this.y + y;
			double cz = this.z + z;
			return new Vector3D(cx, cy, cz);
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the difference of this and that vector.
		 * 
		 * @param that
		 *            Vector to compute difference with
		 * @return This minus that
		 */
		public Vector3D minus(Vector3D that) {

			double cx = this.x - that.x;
			double cy = this.y - that.y;
			double cz = this.z - that.z;
			Vector3D c = new Vector3D(cx, cy, cz);
			return c;
		}

		public Vector3D minus(double x, double y, double z) {
			double cx = this.x - x;
			double cy = this.y - y;
			double cz = this.z - z;
			return new Vector3D(cx, cy, cz);
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the product of this vector and the scalar k.
		 * 
		 * @param k
		 *            Scalar to multiply by
		 * @return (this.x * k, this.y * k, this.z * k)
		 */
		public Vector3D times(double k) {

			return times(k, k, k);
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the result (this.x * a, this.y * b, this.z * c).
		 * 
		 * @param a
		 *            Scalar to multiply x by
		 * @param b
		 *            Scalar to multiply y by
		 * @param c
		 *            Scalar to multiply z by
		 * @return (this.x * a, this.y * b, this.z * c)
		 */
		public Vector3D times(double a, double b, double c) {

			double vx = this.x * a;
			double vy = this.y * b;
			double vz = this.z * c;
			Vector3D v = new Vector3D(vx, vy, vz);
			return v;
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the unit vector in the direction of this vector.
		 * 
		 * @return Unit vector with direction of this vector
		 */
		public Vector3D direction() {

			if (this.mag() == 0.0)
				throw new RuntimeException("Zero-vector has no direction");
			return this.times(1.0 / this.mag());
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the projection of this onto the given line.
		 * 
		 * @param line
		 *            Direction to project this vector onto
		 * @return This projected onto line
		 */
		public Vector3D proj(Vector3D line) {

			Vector3D normal = line.direction();

			return normal.times(this.dot(normal));
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns the cross product of this vector with that vector.
		 * 
		 * @return This cross that
		 */
		public Vector3D cross(Vector3D that) {

			Vector3D a = this;
			Vector3D b = that;

			return new Vector3D(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
		}

		// --------------------------------------------------------------------------

		/**
		 * Reflects this vector across the direction given by line.
		 * 
		 * @param line
		 *            Direction to reflect this vector over
		 * @return This reflected over line
		 */
		public Vector3D reflect(Vector3D line) {

			return this.proj(line).times(2).minus(this);
		}

		// --------------------------------------------------------------------------

		/**
		 * Returns a string representation of this vector.
		 * 
		 * @return "( this.x, this.y, this.z)"
		 */
		public String toString() {

			DecimalFormat df = new DecimalFormat("0.000000");
			return ("( " + df.format(this.x) + ", " + df.format(this.y) + ", " + df.format(this.z) + " )");
		}

		// --------------------------------------------------------------------------

		/**
		 * Draws this vector as a point from the origin to StdDraw3D.
		 */
		public void draw() {

			StdDraw3D.sphere(this.x, this.y, this.z, 0.01);
		}

		// --------------------------------------------------------------------------

		/**
		 * Draws a line representation of this vector, from the given origin.
		 * 
		 * @param origin
		 *            Origin point to draw from
		 */
		/*
		 * public void drawLine (Vector3D origin) {
		 * 
		 * Vector3D end = this.plus(origin);
		 * 
		 * StdDraw3D.line(origin.x, origin.y, origin.z, end.x, end.y, end.z); }
		 */
	}

	public static void main(String[] args) {

		// Sets the scale
		StdDraw3D.setScale(-1, 1);

		// Turns off the default info HUD display.
		StdDraw3D.setInfoDisplay(false);

		// Draws the white square border.
		StdDraw3D.setPenColor(StdDraw3D.WHITE);
		StdDraw3D.overlaySquare(0, 0, 0.98);

		// Draws the two red circles.
		StdDraw3D.setPenRadius(0.06);
		StdDraw3D.setPenColor(StdDraw3D.RED, 220);
		StdDraw3D.overlayCircle(0, 0, 0.8);
		StdDraw3D.setPenColor(StdDraw3D.RED, 220);
		StdDraw3D.overlayCircle(0, 0, 0.6);

		// Draws the information text.
		StdDraw3D.setPenColor(StdDraw3D.WHITE);
		StdDraw3D.overlayText(0, 0.91, "Standard Draw 3D - Test Program");
		StdDraw3D.overlayText(0, -0.95, "You should see rotating text. Drag the mouse to orbit.");

		// Creates the 3D text object and centers it.
		StdDraw3D.setPenColor(StdDraw3D.YELLOW);
		StdDraw3D.setFont(new Font("Arial", Font.BOLD, 16));
		StdDraw3D.Shape text = StdDraw3D.text3D(0, 0, 0, "StdDraw3D");
		text.scale(3.5);
		text.move(-0.7, -0.1, 0);
		text = StdDraw3D.combine(text);

		while (true) {

			// Rotates the 3D text by 1.2 degrees along the y-axis.
			text.rotate(0, 1.2, 0);

			// Shows the frame for 20 milliseconds.
			StdDraw3D.show(20);
		}
	}
}
